diff options
16 files changed, 575 insertions, 84 deletions
diff --git a/schema/changelog-5.6.xml b/schema/changelog-5.6.xml index 335f7b3a8..38b206902 100644 --- a/schema/changelog-5.6.xml +++ b/schema/changelog-5.6.xml @@ -12,6 +12,68 @@ <column name="motionstreak" type="BOOLEAN" defaultValueBoolean="false" /> </addColumn> + <createTable tableName="tc_reports"> + <column name="id" type="INT" autoIncrement="true"> + <constraints primaryKey="true" /> + </column> + <column name="type" type="VARCHAR(32)"> + <constraints nullable="false" /> + </column> + <column name="description" type="VARCHAR(128)"> + <constraints nullable="false" /> + </column> + <column name="calendarid" type="INT"> + <constraints nullable="false" /> + </column> + <column name="from" type="TIMESTAMP"> + <constraints nullable="false" /> + </column> + <column name="to" type="TIMESTAMP"> + <constraints nullable="false" /> + </column> + <column name="attributes" type="VARCHAR(4000)"> + <constraints nullable="false" /> + </column> + </createTable> + + <addForeignKeyConstraint baseTableName="tc_reports" baseColumnNames="calendarid" constraintName="fk_reports_calendarid" referencedTableName="tc_calendars" referencedColumnNames="id" onDelete="CASCADE" /> + + <createTable tableName="tc_user_report"> + <column name="userid" type="INT"> + <constraints nullable="false" /> + </column> + <column name="reportid" type="INT"> + <constraints nullable="false" /> + </column> + </createTable> + + <addForeignKeyConstraint baseTableName="tc_user_report" baseColumnNames="userid" constraintName="fk_user_report_userid" referencedTableName="tc_users" referencedColumnNames="id" onDelete="CASCADE" /> + <addForeignKeyConstraint baseTableName="tc_user_report" baseColumnNames="reportid" constraintName="fk_user_report_reportid" referencedTableName="tc_reports" referencedColumnNames="id" onDelete="CASCADE" /> + + <createTable tableName="tc_group_report"> + <column name="groupid" type="INT"> + <constraints nullable="false" /> + </column> + <column name="reportid" type="INT"> + <constraints nullable="false" /> + </column> + </createTable> + + <addForeignKeyConstraint baseTableName="tc_group_report" baseColumnNames="groupid" constraintName="fk_group_report_groupid" referencedTableName="tc_groups" referencedColumnNames="id" onDelete="CASCADE" /> + <addForeignKeyConstraint baseTableName="tc_group_report" baseColumnNames="reportid" constraintName="fk_group_report_reportid" referencedTableName="tc_reports" referencedColumnNames="id" onDelete="CASCADE" /> + + <createTable tableName="tc_device_report"> + <column name="deviceid" type="INT"> + <constraints nullable="false" /> + </column> + <column name="reportid" type="INT"> + <constraints nullable="false" /> + </column> + </createTable> + + <addForeignKeyConstraint baseTableName="tc_device_report" baseColumnNames="deviceid" constraintName="fk_device_report_deviceid" referencedTableName="tc_devices" referencedColumnNames="id" onDelete="CASCADE" /> + <addForeignKeyConstraint baseTableName="tc_device_report" baseColumnNames="reportid" constraintName="fk_device_report_reportid" referencedTableName="tc_reports" referencedColumnNames="id" onDelete="CASCADE" /> + </changeSet> </databaseChangeLog> diff --git a/src/main/java/org/traccar/api/resource/CommandResource.java b/src/main/java/org/traccar/api/resource/CommandResource.java index 6ef6ee9c5..3460cf6e0 100644 --- a/src/main/java/org/traccar/api/resource/CommandResource.java +++ b/src/main/java/org/traccar/api/resource/CommandResource.java @@ -105,7 +105,6 @@ public class CommandResource extends ExtendedObjectResource<Command> { @POST @Path("send") public Response send(Command entity) throws Exception { - permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); if (entity.getId() > 0) { permissionsService.checkPermission(baseClass, getUserId(), entity.getId()); long deviceId = entity.getDeviceId(); diff --git a/src/main/java/org/traccar/api/resource/ReportResource.java b/src/main/java/org/traccar/api/resource/ReportResource.java index 70177dd4d..6944de9cb 100644 --- a/src/main/java/org/traccar/api/resource/ReportResource.java +++ b/src/main/java/org/traccar/api/resource/ReportResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2023 Anton Tananaev (anton@traccar.org) * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,27 +19,23 @@ package org.traccar.api.resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.traccar.api.BaseResource; -import org.traccar.mail.MailManager; import org.traccar.helper.LogAction; import org.traccar.model.Event; import org.traccar.model.Position; -import org.traccar.model.User; import org.traccar.model.UserRestrictions; import org.traccar.reports.EventsReportProvider; import org.traccar.reports.RouteReportProvider; import org.traccar.reports.StopsReportProvider; import org.traccar.reports.SummaryReportProvider; import org.traccar.reports.TripsReportProvider; +import org.traccar.reports.common.ReportExecutor; +import org.traccar.reports.common.ReportMailer; import org.traccar.reports.model.StopReportItem; import org.traccar.reports.model.SummaryReportItem; import org.traccar.reports.model.TripReportItem; import org.traccar.storage.StorageException; -import javax.activation.DataHandler; import javax.inject.Inject; -import javax.mail.MessagingException; -import javax.mail.internet.MimeBodyPart; -import javax.mail.util.ByteArrayDataSource; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -51,9 +47,6 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; import java.util.Collection; import java.util.Date; import java.util.List; @@ -83,31 +76,11 @@ public class ReportResource extends BaseResource { private TripsReportProvider tripsReportProvider; @Inject - private MailManager mailManager; + private ReportMailer reportMailer; - private interface ReportExecutor { - void execute(OutputStream stream) throws StorageException, IOException; - } - - private Response executeReport( - long userId, boolean mail, ReportExecutor executor) { + private Response executeReport(long userId, boolean mail, ReportExecutor executor) { if (mail) { - new Thread(() -> { - try { - var stream = new ByteArrayOutputStream(); - executor.execute(stream); - - MimeBodyPart attachment = new MimeBodyPart(); - attachment.setFileName("report.xlsx"); - attachment.setDataHandler(new DataHandler(new ByteArrayDataSource( - stream.toByteArray(), "application/octet-stream"))); - - User user = permissionsService.getUser(userId); - mailManager.sendMessage(user, "Report", "The report is in the attachment.", attachment); - } catch (StorageException | IOException | MessagingException e) { - LOGGER.warn("Report failed", e); - } - }).start(); + reportMailer.sendAsync(userId, executor); return Response.noContent().build(); } else { StreamingOutput stream = output -> { diff --git a/src/main/java/org/traccar/handler/ComputedAttributesHandler.java b/src/main/java/org/traccar/handler/ComputedAttributesHandler.java index c9f1f63d7..620852502 100644 --- a/src/main/java/org/traccar/handler/ComputedAttributesHandler.java +++ b/src/main/java/org/traccar/handler/ComputedAttributesHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2023 Anton Tananaev (anton@traccar.org) * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -116,17 +116,45 @@ public class ComputedAttributesHandler extends BaseDataHandler { } if (result != null) { try { - switch (attribute.getType()) { - case "number": - Number numberValue = (Number) result; - position.getAttributes().put(attribute.getAttribute(), numberValue); + switch (attribute.getAttribute()) { + case "valid": + position.setValid((Boolean) result); break; - case "boolean": - Boolean booleanValue = (Boolean) result; - position.getAttributes().put(attribute.getAttribute(), booleanValue); + case "latitude": + position.setLatitude(((Number) result).doubleValue()); + break; + case "longitude": + position.setLongitude(((Number) result).doubleValue()); + break; + case "altitude": + position.setAltitude(((Number) result).doubleValue()); + break; + case "speed": + position.setSpeed(((Number) result).doubleValue()); + break; + case "course": + position.setCourse(((Number) result).doubleValue()); + break; + case "address": + position.setAddress((String) result); + break; + case "accuracy": + position.setAccuracy(((Number) result).doubleValue()); break; default: - position.getAttributes().put(attribute.getAttribute(), result.toString()); + switch (attribute.getType()) { + case "number": + Number numberValue = (Number) result; + position.getAttributes().put(attribute.getAttribute(), numberValue); + break; + case "boolean": + Boolean booleanValue = (Boolean) result; + position.getAttributes().put(attribute.getAttribute(), booleanValue); + break; + default: + position.getAttributes().put(attribute.getAttribute(), result.toString()); + } + break; } } catch (ClassCastException error) { LOGGER.warn("Attribute cast error", error); diff --git a/src/main/java/org/traccar/helper/ClassScanner.java b/src/main/java/org/traccar/helper/ClassScanner.java index c928f6a12..c201d101f 100644 --- a/src/main/java/org/traccar/helper/ClassScanner.java +++ b/src/main/java/org/traccar/helper/ClassScanner.java @@ -46,7 +46,7 @@ public final class ClassScanner { URL packageUrl = baseClass.getClassLoader().getResource(packagePath); if (packageUrl.getProtocol().equals("jar")) { - String jarFileName = URLDecoder.decode(packageUrl.getFile(), StandardCharsets.UTF_8.name()); + String jarFileName = URLDecoder.decode(packageUrl.getFile(), StandardCharsets.UTF_8); try (JarFile jf = new JarFile(jarFileName.substring(5, jarFileName.indexOf("!")))) { Enumeration<JarEntry> jarEntries = jf.entries(); while (jarEntries.hasMoreElements()) { diff --git a/src/main/java/org/traccar/helper/StringUtil.java b/src/main/java/org/traccar/helper/StringUtil.java new file mode 100644 index 000000000..9b4d717f4 --- /dev/null +++ b/src/main/java/org/traccar/helper/StringUtil.java @@ -0,0 +1,32 @@ +/* + * 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.helper; + +public final class StringUtil { + + private StringUtil() { + } + + public static boolean containsHex(String value) { + for (char c : value.toCharArray()) { + if (c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/org/traccar/model/Report.java b/src/main/java/org/traccar/model/Report.java new file mode 100644 index 000000000..83bb2e920 --- /dev/null +++ b/src/main/java/org/traccar/model/Report.java @@ -0,0 +1,65 @@ +/* + * 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.model; + +import org.traccar.storage.StorageName; + +import java.util.Date; + +@StorageName("tc_reports") +public class Report extends ScheduledModel { + + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + private String description; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + private Date from; + + public Date getFrom() { + return from; + } + + public void setFrom(Date from) { + this.from = from; + } + + private Date to; + + public Date getTo() { + return to; + } + + public void setTo(Date to) { + this.to = to; + } + +} diff --git a/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java b/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java index fc8a49cf5..d4bd45c4f 100644 --- a/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/GalileoProtocolDecoder.java @@ -23,6 +23,7 @@ import org.traccar.BaseProtocolDecoder; import org.traccar.NetworkMessage; import org.traccar.Protocol; import org.traccar.helper.BitBuffer; +import org.traccar.helper.BitUtil; import org.traccar.helper.UnitsConverter; import org.traccar.model.Position; import org.traccar.session.DeviceSession; @@ -66,7 +67,7 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { }; int[] l3 = { 0x63, 0x64, 0x6f, 0x5d, 0x65, 0x66, 0x67, 0x68, - 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0xfa }; int[] l4 = { 0x20, 0x33, 0x44, 0x90, 0xc0, 0xc2, 0xc3, 0xd3, @@ -88,6 +89,8 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { } TAG_LENGTH_MAP.put(0x5b, 7); // variable length TAG_LENGTH_MAP.put(0x5c, 68); + TAG_LENGTH_MAP.put(0xfd, 8); + TAG_LENGTH_MAP.put(0xfe, 8); } private static int getTagLength(int tag) { @@ -239,6 +242,8 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { } } else if (header == 0x07) { return decodePhoto(channel, remoteAddress, buf); + } else if (header == 0x08) { + return decodeCompressedPositions(channel, remoteAddress, buf); } return null; @@ -259,7 +264,7 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { position.setValid(bits.readUnsigned(1) == 0); position.setLongitude(360 * bits.readUnsigned(22) / 4194304.0 - 180); - position.setLatitude(360 * bits.readUnsigned(21) / 2097152.0 - 90); + position.setLatitude(180 * bits.readUnsigned(21) / 2097152.0 - 90); if (bits.readUnsigned(1) > 0) { position.set(Position.KEY_ALARM, Position.ALARM_GENERAL); } @@ -267,10 +272,10 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { private Position decodeIridiumPosition(Channel channel, SocketAddress remoteAddress, ByteBuf buf) { - buf.readUnsignedShortLE(); // length + buf.readUnsignedShort(); // length buf.skipBytes(3); // identification header - buf.readUnsignedIntLE(); // index + buf.readUnsignedInt(); // index DeviceSession deviceSession = getDeviceSession( channel, remoteAddress, buf.readSlice(15).toString(StandardCharsets.US_ASCII)); @@ -283,12 +288,19 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { buf.readUnsignedByte(); // session status buf.skipBytes(4); // reserved - buf.readUnsignedIntLE(); // date and time + position.setTime(new Date(buf.readUnsignedInt() * 1000)); - buf.skipBytes(23); // coordinates block + buf.skipBytes(3); // coordinates header + int flags = buf.readUnsignedByte(); + double latitude = buf.readUnsignedByte() + buf.readUnsignedShort() / 60000.0; + double longitude = buf.readUnsignedByte() + buf.readUnsignedShort() / 60000.0; + position.setLatitude(BitUtil.check(flags, 1) ? -latitude : latitude); + position.setLongitude(BitUtil.check(flags, 0) ? -longitude : longitude); + buf.readUnsignedInt(); // accuracy - buf.skipBytes(3); // data tag header - decodeMinimalDataSet(position, buf); + buf.readUnsignedByte(); // data tag header + // ByteBuf data = buf.readSlice(buf.readUnsignedShort()); + // decodeMinimalDataSet(position, data); return position; } @@ -392,4 +404,43 @@ public class GalileoProtocolDecoder extends BaseProtocolDecoder { return position; } + private List<Position> decodeCompressedPositions(Channel channel, SocketAddress remoteAddress, ByteBuf buf) { + + buf.readUnsignedShortLE(); // length + + DeviceSession deviceSession = getDeviceSession(channel, remoteAddress); + if (deviceSession == null) { + return null; + } + + List<Position> positions = new LinkedList<>(); + while (buf.readableBytes() > 2) { + + Position position = new Position(getProtocolName()); + position.setDeviceId(deviceSession.getDeviceId()); + + decodeMinimalDataSet(position, buf); + + int[] tags = new int[BitUtil.to(buf.readUnsignedByte(), 8)]; + for (int i = 0; i < tags.length; i++) { + tags[i] = buf.readUnsignedByte(); + } + + for (int tag : tags) { + decodeTag(position, buf, tag); + } + + positions.add(position); + + } + + sendResponse(channel, 0x02, buf.readUnsignedShortLE()); + + for (Position p : positions) { + p.setDeviceId(deviceSession.getDeviceId()); + } + + return positions.isEmpty() ? null : positions; + } + } diff --git a/src/main/java/org/traccar/protocol/WatchProtocolDecoder.java b/src/main/java/org/traccar/protocol/WatchProtocolDecoder.java index 142d1b64f..40d56b130 100644 --- a/src/main/java/org/traccar/protocol/WatchProtocolDecoder.java +++ b/src/main/java/org/traccar/protocol/WatchProtocolDecoder.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. @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import org.traccar.BaseProtocolDecoder; +import org.traccar.helper.StringUtil; import org.traccar.session.DeviceSession; import org.traccar.NetworkMessage; import org.traccar.Protocol; @@ -139,41 +140,45 @@ public class WatchProtocolDecoder extends BaseProtocolDecoder { String[] values = parser.next().split(","); int index = 0; - Network network = new Network(); + if (values.length < 4 || !StringUtil.containsHex(values[index + 3])) { - int cellCount = Integer.parseInt(values[index++]); - if (cellCount > 0) { - index += 1; // timing advance - int mcc = !values[index].isEmpty() ? Integer.parseInt(values[index++]) : 0; - int mnc = !values[index].isEmpty() ? Integer.parseInt(values[index++]) : 0; + Network network = new Network(); - for (int i = 0; i < cellCount; i++) { - int lac = Integer.parseInt(values[index++]); - int cid = Integer.parseInt(values[index++]); - String rssi = values[index++]; - if (!rssi.isEmpty()) { - network.addCellTower(CellTower.from(mcc, mnc, lac, cid, Integer.parseInt(rssi))); - } else { - network.addCellTower(CellTower.from(mcc, mnc, lac, cid)); + int cellCount = Integer.parseInt(values[index++]); + if (cellCount > 0) { + index += 1; // timing advance + int mcc = !values[index].isEmpty() ? Integer.parseInt(values[index++]) : 0; + int mnc = !values[index].isEmpty() ? Integer.parseInt(values[index++]) : 0; + + for (int i = 0; i < cellCount; i++) { + int lac = Integer.parseInt(values[index], StringUtil.containsHex(values[index++]) ? 16 : 10); + int cid = Integer.parseInt(values[index], StringUtil.containsHex(values[index++]) ? 16 : 10); + String rssi = values[index++]; + if (!rssi.isEmpty()) { + network.addCellTower(CellTower.from(mcc, mnc, lac, cid, Integer.parseInt(rssi))); + } else { + network.addCellTower(CellTower.from(mcc, mnc, lac, cid)); + } } } - } - if (index < values.length && !values[index].isEmpty()) { - int wifiCount = Integer.parseInt(values[index++]); + if (index < values.length && !values[index].isEmpty()) { + int wifiCount = Integer.parseInt(values[index++]); - for (int i = 0; i < wifiCount; i++) { - index += 1; // wifi name - String macAddress = values[index++]; - String rssi = values[index++]; - if (!macAddress.isEmpty() && !macAddress.equals("0") && !rssi.isEmpty()) { - network.addWifiAccessPoint(WifiAccessPoint.from(macAddress, Integer.parseInt(rssi))); + for (int i = 0; i < wifiCount; i++) { + index += 1; // wifi name + String macAddress = values[index++]; + String rssi = values[index++]; + if (!macAddress.isEmpty() && !macAddress.equals("0") && !rssi.isEmpty()) { + network.addWifiAccessPoint(WifiAccessPoint.from(macAddress, Integer.parseInt(rssi))); + } } } - } - if (network.getCellTowers() != null || network.getWifiAccessPoints() != null) { - position.setNetwork(network); + if (network.getCellTowers() != null || network.getWifiAccessPoints() != null) { + position.setNetwork(network); + } + } return position; @@ -263,7 +268,7 @@ public class WatchProtocolDecoder extends BaseProtocolDecoder { Position position = decodePosition(deviceSession, buf.toString(StandardCharsets.US_ASCII)); if (type.startsWith("AL")) { - if (position != null) { + if (position != null && !position.hasAttribute(Position.KEY_ALARM)) { position.set(Position.KEY_ALARM, Position.ALARM_GENERAL); } sendResponse(channel, id, index, "AL"); @@ -279,6 +284,7 @@ public class WatchProtocolDecoder extends BaseProtocolDecoder { || type.equalsIgnoreCase("HEART") || type.equalsIgnoreCase("BLOOD") || type.equalsIgnoreCase("BPHRT") + || type.equalsIgnoreCase("TEMP") || type.equalsIgnoreCase("btemp2")) { if (buf.isReadable()) { @@ -291,7 +297,9 @@ public class WatchProtocolDecoder extends BaseProtocolDecoder { String[] values = buf.toString(StandardCharsets.US_ASCII).split(","); int valueIndex = 0; - if (type.equalsIgnoreCase("btemp2")) { + if (type.equalsIgnoreCase("TEMP")) { + position.set(Position.PREFIX_TEMP + 1, Double.parseDouble(values[valueIndex])); + } else if (type.equalsIgnoreCase("btemp2")) { if (Integer.parseInt(values[valueIndex++]) > 0) { position.set(Position.PREFIX_TEMP + 1, Double.parseDouble(values[valueIndex])); } diff --git a/src/main/java/org/traccar/reports/common/ReportExecutor.java b/src/main/java/org/traccar/reports/common/ReportExecutor.java new file mode 100644 index 000000000..aed4b8c23 --- /dev/null +++ b/src/main/java/org/traccar/reports/common/ReportExecutor.java @@ -0,0 +1,25 @@ +/* + * 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.reports.common; + +import org.traccar.storage.StorageException; + +import java.io.IOException; +import java.io.OutputStream; + +public interface ReportExecutor { + void execute(OutputStream stream) throws StorageException, IOException; +} diff --git a/src/main/java/org/traccar/reports/common/ReportMailer.java b/src/main/java/org/traccar/reports/common/ReportMailer.java new file mode 100644 index 000000000..1723c0e3b --- /dev/null +++ b/src/main/java/org/traccar/reports/common/ReportMailer.java @@ -0,0 +1,69 @@ +/* + * 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.reports.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.mail.MailManager; +import org.traccar.model.User; +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 javax.activation.DataHandler; +import javax.inject.Inject; +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; +import javax.mail.util.ByteArrayDataSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class ReportMailer { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReportMailer.class); + + private final Storage storage; + private final MailManager mailManager; + + @Inject + public ReportMailer(Storage storage, MailManager mailManager) { + this.storage = storage; + this.mailManager = mailManager; + } + + public void sendAsync(long userId, ReportExecutor executor) { + new Thread(() -> { + try { + var stream = new ByteArrayOutputStream(); + executor.execute(stream); + + MimeBodyPart attachment = new MimeBodyPart(); + attachment.setFileName("report.xlsx"); + attachment.setDataHandler(new DataHandler(new ByteArrayDataSource( + stream.toByteArray(), "application/octet-stream"))); + + User user = storage.getObject( + User.class, new Request(new Columns.All(), new Condition.Equals("id", userId))); + mailManager.sendMessage(user, "Report", "The report is in the attachment.", attachment); + } catch (StorageException | IOException | MessagingException e) { + LOGGER.warn("Email report failed", e); + } + }).start(); + } + +} diff --git a/src/main/java/org/traccar/schedule/ScheduleManager.java b/src/main/java/org/traccar/schedule/ScheduleManager.java index 6412a186a..e1de3b3af 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 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2020 - 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. @@ -38,8 +38,12 @@ public class ScheduleManager implements LifecycleObject { @Override public void start() { executor = Executors.newSingleThreadScheduledExecutor(); - List.of(TaskDeviceInactivityCheck.class, TaskWebSocketKeepalive.class, TaskHealthCheck.class) - .forEach(task -> injector.getInstance(task).schedule(executor)); + var tasks = List.of( + TaskReports.class, + TaskDeviceInactivityCheck.class, + TaskWebSocketKeepalive.class, + TaskHealthCheck.class); + tasks.forEach(task -> injector.getInstance(task).schedule(executor)); } @Override diff --git a/src/main/java/org/traccar/schedule/TaskReports.java b/src/main/java/org/traccar/schedule/TaskReports.java new file mode 100644 index 000000000..259eb10d4 --- /dev/null +++ b/src/main/java/org/traccar/schedule/TaskReports.java @@ -0,0 +1,154 @@ +/* + * 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.schedule; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.model.BaseModel; +import org.traccar.model.Calendar; +import org.traccar.model.Device; +import org.traccar.model.Group; +import org.traccar.model.Report; +import org.traccar.model.User; +import org.traccar.reports.EventsReportProvider; +import org.traccar.reports.RouteReportProvider; +import org.traccar.reports.StopsReportProvider; +import org.traccar.reports.SummaryReportProvider; +import org.traccar.reports.TripsReportProvider; +import org.traccar.reports.common.ReportMailer; +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 javax.inject.Inject; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class TaskReports implements ScheduleTask { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskReports.class); + + private static final long CHECK_PERIOD_MINUTES = 1; + + private final Storage storage; + private final ReportMailer reportMailer; + + @Inject + private EventsReportProvider eventsReportProvider; + + @Inject + private RouteReportProvider routeReportProvider; + + @Inject + private StopsReportProvider stopsReportProvider; + + @Inject + private SummaryReportProvider summaryReportProvider; + + @Inject + private TripsReportProvider tripsReportProvider; + + @Inject + public TaskReports(Storage storage, ReportMailer reportMailer) { + this.storage = storage; + this.reportMailer = reportMailer; + } + + @Override + public void schedule(ScheduledExecutorService executor) { + executor.scheduleAtFixedRate(this, CHECK_PERIOD_MINUTES, CHECK_PERIOD_MINUTES, TimeUnit.MINUTES); + } + + @Override + public void run() { + Date currentCheck = new Date(); + Date lastCheck = new Date(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(CHECK_PERIOD_MINUTES)); + + try { + for (Report report : storage.getObjects(Report.class, new Request(new Columns.All()))) { + Calendar calendar = storage.getObject(Calendar.class, new Request( + new Columns.All(), new Condition.Equals("id", report.getCalendarId()))); + if (calendar.checkMoment(currentCheck) && !calendar.checkMoment(lastCheck)) { + executeReport(report); + } + } + } catch (StorageException e) { + LOGGER.warn("Scheduled reports error", e); + } + } + + private void executeReport(Report report) throws StorageException { + var deviceIds = storage.getObjects(Device.class, new Request( + new Columns.Include("id"), + new Condition.Permission(Device.class, Report.class, report.getId()))) + .stream().map(BaseModel::getId).collect(Collectors.toList()); + var groupIds = storage.getObjects(Group.class, new Request( + new Columns.Include("id"), + new Condition.Permission(Group.class, Report.class, report.getId()))) + .stream().map(BaseModel::getId).collect(Collectors.toList()); + var users = storage.getObjects(User.class, new Request( + new Columns.Include("id"), + new Condition.Permission(User.class, Report.class, report.getId()))); + for (User user : users) { + switch (report.getType()) { + case "events": + reportMailer.sendAsync(user.getId(), stream -> { + eventsReportProvider.getExcel( + stream, user.getId(), deviceIds, groupIds, + List.of(), report.getFrom(), report.getTo()); + }); + break; + case "route": + reportMailer.sendAsync(user.getId(), stream -> { + routeReportProvider.getExcel( + stream, user.getId(), deviceIds, groupIds, + report.getFrom(), report.getTo()); + }); + break; + case "summary": + reportMailer.sendAsync(user.getId(), stream -> { + summaryReportProvider.getExcel( + stream, user.getId(), deviceIds, groupIds, + report.getFrom(), report.getTo(), false); + }); + break; + case "trips": + reportMailer.sendAsync(user.getId(), stream -> { + tripsReportProvider.getExcel( + stream, user.getId(), deviceIds, groupIds, + report.getFrom(), report.getTo()); + }); + break; + case "stops": + reportMailer.sendAsync(user.getId(), stream -> { + stopsReportProvider.getExcel( + stream, user.getId(), deviceIds, groupIds, + report.getFrom(), report.getTo()); + }); + break; + default: + LOGGER.warn("Unsupported report type {}", report.getType()); + break; + } + } + } + +} diff --git a/src/test/java/org/traccar/protocol/GalileoProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/GalileoProtocolDecoderTest.java index f676b1cc1..df7f37903 100644 --- a/src/test/java/org/traccar/protocol/GalileoProtocolDecoderTest.java +++ b/src/test/java/org/traccar/protocol/GalileoProtocolDecoderTest.java @@ -10,6 +10,9 @@ public class GalileoProtocolDecoderTest extends ProtocolTest { var decoder = inject(new GalileoProtocolDecoder(null)); + verifyPosition(decoder, binary( + "01004e01001c0747ea59333030323334303639363034353930000012000063c85e6903000b0321a8f846aba50000000202001e205f5ec863300c4643fdfdbbe6c8fb330000000034e7013505d400000000")); + verifyNotNull(decoder, binary( "01006501001c05cf1f8133303032333430363939303130313000004a000062d8f3ee03000b000f85402088970000000602003503333030323334303639393031303130100000207af3d862300cbe08ee00acfaf001330000760634b301350840090a416b2f42920e")); diff --git a/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java index ef82a11a2..f3a77228c 100644 --- a/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java +++ b/src/test/java/org/traccar/protocol/Gl200TextProtocolDecoderTest.java @@ -12,6 +12,10 @@ public class Gl200TextProtocolDecoderTest extends ProtocolTest { var decoder = inject(new Gl200TextProtocolDecoder(null)); verifyAttribute(decoder, buffer( + "+RESP:GTERI,271002,863457051562823,,00000002,,10,1,1,0.0,15,28.2,-58.695253,-34.625413,20230119193305,0722,0007,1168,16B3BB,00,0.0,,,,99,210100,2,1,28F8A149F69A3C25,1,0190,20230119193314,07C7$"), + Position.PREFIX_TEMP + 1, 25.0); + + verifyAttribute(decoder, buffer( "+RESP:GTDAR,F10406,865284049582228,,4,0,,,1,18.5,0,129.4,114.015430,22.537279,20210922004634,0460,0000,27BD,0DFC,,,,20210922004635,082B$"), "warningType", 4); diff --git a/src/test/java/org/traccar/protocol/WatchProtocolDecoderTest.java b/src/test/java/org/traccar/protocol/WatchProtocolDecoderTest.java index 7eb167a74..37fab7e40 100644 --- a/src/test/java/org/traccar/protocol/WatchProtocolDecoderTest.java +++ b/src/test/java/org/traccar/protocol/WatchProtocolDecoderTest.java @@ -17,6 +17,20 @@ public class WatchProtocolDecoderTest extends ProtocolTest { var decoder = inject(new WatchProtocolDecoder(null)); + verifyPosition(decoder, buffer( + "[SG*9059011020*0067*AL,240123,181628,V,54.427538,N,6.409275,W,0.00,0,0,0,19,90,0,0,00000000,1,1,234,10,55C0,3B882A2,132,,10]")); + + verifyPosition(decoder, buffer( + "[SG*9059011020*006b*UD2,240123,162011,A,54.427621,N,6.409190,W,0.00,0,0,8,19,88,0,0,00000000,1,1,FFFF,FFFF,FFFE,3B882A2,132,,00]")); + + verifyAttribute(decoder, buffer( + "[ZJ*5678901234*0001*0009*TEMP,36.5]"), + Position.PREFIX_TEMP + 1, 36.5); + + verifyAttribute(decoder, buffer( + "[ZJ*689466020014198*0003*0113*AL,221121,085515,V,00.000000,N,000.000000,E,0,0,0,0,0,44,0,0,00100000,1,255,460,0,16399,234887445,0,6,WIFI00,68:77:24:1b:e7:a7,-59,WIFI01,68:77:24:1b:e3:30,-75,WIFI02,68:77:24:1b:e3:27,-75,WIFI03,00:41:d2:c0:f2:f1,-76,WIFI04,00:41:d2:c0:f2:f0,-77,WIFI05,68:77:24:1b:e3:d8,-82]"), + Position.KEY_ALARM, Position.ALARM_REMOVING); + verifyNull(decoder, binary( "5b5a4a2a3738393436383035303034323639322a303033342a303433392a4a58544b2c302c77617463685f375f32303232303532363039333935342c312c362c2321414d520a0c0a3c3f96d98367e9468ea245320c0a3c3f96d98367e9468ea245320c0a3c3f96d98367e9468ea245320c389814ffcd762fe49d50ae7a2e0cb528aefbf76911df05c2fbe17d050c2200cff77ef0df4d9b4ab9a4340c449814dbe7c63fa82bc3750d800cc48abbffddb0df8e8fda95e5980c49982ff6cf65f9377d02a39c3aaa0c389805f2ff42c1b80e0a0eb1dc0c2998e9defe15cfa3bdbe80d3540c7298c2f6d9239e3eae3c4a81660c490034dbfd513fedad0c2fc3900cc40039b7f71bb0657ba75558c40cd7813b97ff7219777ec7f401260c7d040003bff6f75fdb898a6ba1140cecd127f7f83357cb73a68a5f680cb081d4f6e749e6af8ed367bc480cec1815dfffd2dbed358112af320ccc21179fffbd17a3a61c133b380c920047defc72a784770ec0fe400c383c42f6faebe76fa736e9d1be0c4918e7decddc67ec9afd87ff220cc418e6bffe6cf6c9ac1f83c3900ca6cad1ffe3da24e1be3b547d03c00c6b00b8fee77f60a76d3e7d0292e20c2918b7bfff76387b793d3a36300c7d0400b7fff7f63ae513ac6f74de0c980016fbff7d01f9b30fca67c7220caa982ff6dbd7bf3d8dfed143ec0c44982fdfcb7b517e26f6ea52420c0ed19cfedb438f179d3fc50ec40c008ad2ffff635fe28dc1ec1f860c76cb7d05bffda53eaebe4d201ae20ccccaffbbfd3db4abb6ddf39d0e0c6b00acfeff406fe4a12661caf80c76982fbffffeb17b2f65472f300c7d04c24bf6ef1b10e47f73fc10b40c76036cfecd4e7837145a6a8e900ccccaf2beeba36fbaa36feb60640c7d0400dfffff73bfa3b87d0256a1700c7d04e4efd6efc23b651eb73e77780ccc1813ffffb39f6baa6f3195080c00007afffee0b9df6346ca08d00c6bca7d04feff4d64a7b56f9855da0c769819fbfd954ea5bd7d040ba6420c4c4c09bfffd5f6c57d01930e7d01220cc48a22dffe30bfcbaf08a795140c7d040433b7ff3cba5f3e336a40060cecbd2bfaf775bea7bde7e095ee0cc4caaef7fb623feeaab69eabc20c8c0021bffd1ea4a1b090175a920c7d046445fbdfc1abe910b0ca56160c7d0440cbd7ef03893c7f7d02e1a8c20c85e42dff5fdaa145759d326b5a0ccc4084f75f8eeb7b15e4eb4ff80c6bc22ef7d7eaf8df3b8ff678cc0cca4026f3cf7518fd731ceca3560cec041ffbe7655eaf822f04c4fe0c7d0478e3fbfcb5dfc8a81fdbb9e20cc418e6d7ff777f26affe37cc020cd7977cbff7d0aecb9727be6a0c0c00bde1f2d797fc8754ed09d4ae0cc498279f7e729fcb9eff70c1a60c7d0478e5bfffc67eef953fda69aa0c7200f1bbf7e891ef5a287cb5b80c983629fee5555f739f8a29279a0c76d103bef73f55a6bf89ba8ce40cc46424f6ded9a194167a067d05880cc4780efeffc37fae944d89bfb40c4464bffffb6dd54e8344394a5c0c49781ffffc653feaa31af59ac00cc464cebfd76ae4e8a55d5b5a4a2a3738393436383035303034323639322a303033352a303434332a4a58544b2c302c77617463685f375f32303232303532363039333935342c322c362c5f24fbbc0c7d04983effcfe35fbd8fff7ce6f60c448a1dfbd715bec3b42448e58a0c0e0421bf5f17ebc31a43301bc40c768ababf4f66cf629ee31fe9900c6b6426bf5f4eb7669dacc439140c945733bff9351e7d04ab6be54ef60ccc98bfbffbf67f63b78d8f42880c7d0400c6f7fffab5cbae8e8275da0c0097ffdefff5dfafb0c4727d04980ccc78d1f6ff1a6b7e37631059fa0c760045f3fd853fea877d03aa87440cc40022f7ffa8b5c58f6e80c58e0ccc4c1f9ffff32fa7af87de04560cb09801f6fdd32efcbdad9495b60c6b987d05bff72d61eb687d05a9f9e20cca57c7ffff6cb1fe3387ea596e0c629823dedfbfc50b97965e44ea0c4918ccbfd7cc75e59fff92e9ee0c8c78e7faff707ffda60ec3ada60c30c217befffcb8f3290f06d75e0c0e8a7bb7dff3eec7b7928ef6680c38ca1cdeff272ba012597d051a520c0018dfffeff27d01e369b3bcd7d80c98146eb2fc7f45c38e1ce53e120c3d5700ff7fd44bee28aa089d900c007157bffffe30302d7d0583585c0ccc5747bfe7f73cff7d02de1098240c7d047865f6feca71d70bdfaab6a20cecb946bfd966be74b61a06c6660c441612ffe7eb966a8c2886cf760ccc7827bffd653ff7980a62e9320c00ca98f7fc1b311a3986700cc20c490017bfff70cfe7bb455637320c6228afbf7e6da4c59763b693740cccc2b5fee1951975760e4a9dca0c0000faffdb255f6f86af4be6fc0cccc2ffdfff7d0225efbc847c42760cb48a8fdffbe23ff1a78782cbe60c7d04980cffff6e6dfc75a6c65a500ce62808bfff24f853748a9537400cb4004ebfeb6e70aa4314903e8c0cb46408ff72d62f44945f64897d040cccca7abfff2db5c6bfcc345e700c7d04c24dd6fd5f95638f78974a800cc4caffffffb17d01c36bdd7d047c2e0c7d0436d1f7f8917f83af7fc5c2180c070449f6fe570f128577c8c97d040c7d047813f6df3fd17d026b6790d1b80cc400c9d6d92dc56497843ed10a0c44986effffe13fef8eef7c717a0c7d049872bbfdad3abe2be0d60b240c4900adbf7d02dfa4ef960fdf23580c629845fefc5a650fbf8b0d4cda0c7d040076b7ef273fe78fce983ae40c4a00ded7ff629f7fab4531501c0c6200b7ffedf13fe6a68f01fb6c0cc49809f77fdb91517fb39e8ba00c760009bf7fd6afe7b46921f27a0cecbd1df6faf99120776e807d03fe0c98ca23fbff3749fb5ed3909c160cc44cbbbfffbd70e77d0367bc2e740c499816f77fe2ffe08edd7d048c9e0c940021beff1df0f1338c2f1a3a0c7d040083feebba1c756e5760c2200c94981dfffdf921b92b99909ba40c7d04984ebbdf7ff0c77d025a1fbfc00c7657b3ff77f4befcae2bf625640c62ca53f7fdb35f2e92af3bd5c60c7d049878bbdfc0dfc1bdaa1918000cc404bbbfefb41ee6b50a39bf180c767874f7fb8ee156320ee600020c69981cffffe0fff6bb2764acaa0cec781bfbff7d0324eb9b4b4d5d5b5a4a2a3738393436383035303034323639322a303033362a303433302a4a58544b2c302c77617463685f375f32303232303532363039333935342c332c362c73580c07ca4fbbfff5bec5abcfac465c0c0042a8beff316aed38fe4603dc0c7d04577d04fefdc929f7294ce7520c0c76984dfaff402fe68ebe113b000c7d0436a6fed465ff4f8d8e3306300c44e434f7f79e84abb8fa081be80c983636bef7e128bf7d03d34c71c80cd7982dffd54cac94536beab1320c058bd6bfff701fe69d1a39d1e80c76caedb77ffea0e47febb469440c0098b2bef5e55eed981672b1c40cd800d1bef4bda58daf7d01d309180c08cadffbfc31799f5613fab2da0c7d049828beffa5eea0bd09a14c2e0c0000e59feb5f9dff7f4945e98e0cc40046ffcfe57fb58f7d053dd9e20c7d040047d7ffd38f67aa9db8d38c0c7698d2df5fe2efc6bb0f5a18220c980367bbfd95dae73e3bf2b59a0c49813796fdf5efab9d3ed7d3d80c9800ebd7fdb831ae8f91811e920c7d04ca74bbff5cdcf45605c25f140c013e029fbfe7ffc299376b409a0c7510009e7d02010f68824cccead60cb13b00f7d5821e6c8d594a06880c75ec00f6e782e8f06b0c610d1a0cc09843dfef117fa5b3aef236f40cdcc208ffff4831f83703ca20c00cc09800bf7fd71eec9ba34e3c200c650f6bffd5bc4dbe7ec5b0f1d80c0d1c1f92ef917fa0b96023eac00c8efb1db6bfa42eed9f69051d4e0c8eff23b2fe4486d56ef44f702e0c8ebc1b9657d46eeea852ac337e0c8efb1f96d9877fc9b34c38f1540c121c2796df13577c727f8cedec0ca9107d05afdd6a9d957ab76c02560c121c2dd6bfd33fa5b844f27d02340cb11037b6e3962e6eaf52d6e7460c2b1c3db2dfe61eef9aad3dcc0a0c542e42d763e709fe723a83ae7c0c7cfa56bf7fedb8763c3f8ab2c00ca40022efff2411604d22a485e00c228adcbfdfcd748293c66b0bf20cb10f0097ff806dcf7a0a640d6e0c5a0400b7e3030ef4ae0a6046f80cb82812fe7b04d77e4bab11894a0c541c24b2dd53fea5891e3824420ca9fb26b2d7156fa485049cac420ca91c2d9edb2487526c2e2260fa0ca9bc25b65ff28e41a34232c0f00c8efb25b649e51ec39c0ae6703a0c70f425a6c820dfaf2f8124c8960c80f42794b323aed9ca9b6924be0cb93b278e3e8d929c36bb70b6360c587b27524f85857d056d214841660cda2f278b4c876eb3186e5acc540ce6832f6e3f0d82f81226e6dc3a0c0b922feff50a03d03c98ae28360c03d62befb54ab611de0c8addee0c84d626b7fdb4461556dc0153040c0b0f27ff5781c5c2b32f6762140c49bf2dfaf521969b702ecb11d80c7d050f27cfed75bfab95dc4af3180c011c3d8e7451df80a5cd0948080c7c2e35f2c7b0becca22e50769e0c7c1c3d965e628f46a9a58ae6100c7c2e3b965f4a6b5ebd1436b0b00c7c863eaedf18c7816cdeeed7e20c2b3e3df6b73952b2b1abc048a00c7cfb3db6cd67ae68cda18af9d60c2bff3e96df12ff61b39627cd580c7c863fd6cd810ee595d85ee6645d5b5a4a2a3738393436383035303034323639322a303033372a303433372a4a58544b2c302c77617463685f375f32303232303532363039333935342c342c362c0c7cff3fb65f812fe196dc62d8360c7c2e3db2de738fae9ca7c7236c0c2b2e3d96df6c1d775a3df5a47e0c7c3e3f96fe943dc07e81e170700c75503fb7d6a6197f1b08df85440c5a133ab6d6a0e68f3acb04b7b00cfa293fd6cfc20c390d5265d3c80c581347b278077d03dc22d9e49c680ce64d47924e8dcdffe9be19dc100cb90f4f9e612d2201961c2e111c0c003b4febb58a96c809ab5a71040ce6d759fbddba54b2aa9d9d983e0cc029576ecf1f703099d84994520c4029579e7d0281577073f5629dd80c5a2952b6fed177177d01e57d0175c20cd8647d044f7e3c58aba523ddb0720cb56753d3e9c6799f95938401f60c756414ff5f00fb57bc4d7e111c0c7c085573ef743ee19ca30057e00c752e5ab77e34eec69f3cbe53940c542e4d96d6e7ff03854a9506620c7cff55b6efca197610a86606180c7cfb55b67eb25f4b999c002bf20c7c107d03beeb451fad8d042492740cb8f05eb5ff664f679c1cc991c60ce1671bffd33d9445b57d0584c7d60c07781dfef5e3ee81b45ff8a8c80cc40072b7dfe6fe6e978257253a0cb1131fe6783035868e439b00d80c583b27b17ecb53dd165c3422180c703b2fbeb18d023e14d90304700c3cce2b93cdc2e6b3550a5546160cb13b3b72fe24273859059b52040c224d34edc742f6bb7c877d02b5940cb84d37cdcfcbc3f03a041cf4ac0ca4923a9f3b74e805b235cce8660c759000fbfd90565072aa7d03bb520cb592017f411dcbe80a0cc420360c09d6528fcf2e25b382ee46d75a0ce2e853d3cfb617fd7d023f05e0780c07cc3bbabfa9f6cec5aa48ac600c404208fb678a4cf16a9c09098c0cf14244fbdfc2afe7932fe57d02900c011c369e7d0264a8570a54f9a2e80cb1103daed6249fe59dc888c2ac0cb1103573c5f697be759923ecc80c705f7d059effaaa4b09b976f375a0c804223bbed89767d041693edab180cb49819d7eb5a8573bb10c951540c3c643af74f021f25b6dd3b7d02040c07982bd73fb31fe38f09a227a40c037610ff7d03c6058098fef936a80c03b808debbe055a1a71319128c0c6b8f2e9e3bb52d71c226d3141a0cdab82d7fa9d416167a1cb950020c03c32bb23ee9e3743f3b6853060cc02937adb5157d05f4a6960c1b680c4076365e6b754bda9394a7aa720c06293ef23313fb1b94bcc2c09a0ce676006bbd8e0417941b90d23c0cb9294f7d01fd3d65748cf6d1cc220c03767d035e3a65e6f2514ab1db060ce6d965bf4035f65c79a9e4446a0ce62921dbf46457b56cd65f6c500c584d6bba7ea8166edd6cf7693a0cb939227bfca25f82a7c4202f2e0c06ce2b9a4f0d95f39eb43c22840c06901eef6fd15fe48f452000e00c62023afbffb269beba0ee0ac540ce6ec00f37b8139f2026e80f1440c22501dfe7d017d03ed8c3ff6803f240cc48ae3b7fd725e63a347cd64100c0094f8efdeb0a9f35c83eb06b00c005d5b5a4a2a3738393436383035303034323639322a303033382a303433332a4a58544b2c302c77617463685f375f32303232303532363039333935342c352c362c6647deef2df57fb68fd76b660c44e43cbfefc0beec867d025c3f920cb4507d01ff6f42ee6c1f58a0db0e0c7d045701ff7f19f9f90fdff037c20c4404259efcd47d02a52b7ef6df0e0c7d04d128f3578db14779993109560ce1e41ecffd2851af49256b1ecc0c7d049837ffff707168db3dd1ac240cc09419f7f39a7d013d6ec1b4e55c0cc4881cff5f49f55fadc52285a40c06ce9ad7dbb8349b97031ac3b00c407d049d9bed74dfadad950eab560c06799ffac7a924b18bdf11c3d40c8054a2f777522e65be921a77240c8042a19bff706fe5b7e3e7d0360cd82840bff3f4fe429bea0081560c0064419fd9157f8b9c797e92c20cc47fe2b7d5e6ee45bf0524010e0c0894299e7d037e4553bcbbc0989e0c40501bbbdc926f8daf53885e0a0c80971ffee559f5bf98ed54cf140c00581abbffac75b88b14d5e3780c5a046dd66fd7bfe5b7ca749df40c403672bbcfe47ee797860ded5e0cb5502efb7ee2ffc7bbdc27d0880c44ca08fbf7031ecd9321d21ef40cdc9806fefde36f0699ad1569420c22cb1bfbdc974fcb900ab601440c00cb1cfbdfd2f7d2730b0b46680c0b4203dfef309fe0adc121e2080c225829feeb901f25b926343bae0c624204dfff4875b69ceda4df4c0c499886ffe7301e8e8bf9ef0a260cdce408dbef829fe45337e98d860c010800ef76088d8015f3b2c84e0c755c3ecfc898f50fbcdb9344220c7c7d03459349337d01e4949da1b4ec0c7cf046ae6e4a90df3552070a5e0c7cf045d23fbba0951147986eca0c7536426e6ef2ef0b7d01c22fd2960cb8284e9db92d40552988334f200cb8024acdbfea90d13b545fe35e0cb8f04d5e48659d48b97c1096140c7c084f79df4402702126bf358a0cb8a74d4def66535e145638d12e0c543e4d8edfa2531b15a340eb800cb83e4eafbd9031291a8563e4780c54a74ef1c7300da79aa94bb67d050cb8f04eba6fb0325813871511b80ce20ff6fffd404ed58ecf264c8e0c7c0f1cb3fe1a36fb7187c379d00c7c3e52f6727d0261ca8bdd1361b80cb8d64d9a4fb1cfe0aadaaa7d05c40c2b2e44b2bfb9b4dcad9d7d0200280c7c3b47d5db803fc2aee71a77f00c7c1343b2bfa06ee6ad3228f10a0c7c3b4a76477fe7650b0c809ca80c54d74b92dbc327134514d6126e0c7c5f4a8f4f74efaaa5cd0a43bc0c7c104bb64ff5ce604fa2028b680c7cff4aaec7f7a9394cc4c599e60cb8ce52eeb553d215a6a466b4120c750f4fae5f9816c04a0a3f751e0c75e04f73c58e96bb4b925a4c680cb80f4b6e3fe1635614bbdeccbc0c7c3b4faf7e825e42d473ee76800ce298578ffcb61f0af936387bbe0cb800e39bf62410681583e0d2b20cb4987d05ff7f297bf69eaf4cd8b40cb57815bf7f300fcffe9fae966a0cb497acd75fd37f648d00d141a00cc48af9fffe1a3b979b8f8573e80c29bff1bf4b94c6fd7c3eec075e0c7cce1f5d5b5a4a2a3738393436383035303034323639322a303033392a303065352a4a58544b2c302c77617463685f375f32303232303532363039333935342c362c362c8f5cf9f34fa6edceb18c0ca4ce436e7d02d329de94b63eb11a0cb894438f44cf753dbc1a83b4560cb8e44391eff729d4155a231bf40cb59443f373b4288492cd6e38160c5a42049bfec8678ad42bce0c560cb12e39fec3d1fe24b5572ee0be0cb1ce35fe33688455aa74e3ffa40cb1d635df4b704160ae864a7d03440cb10f3267dbf4a21d540ca1051c0cb11337724f8747f5547d054e91660cb1a73692df964f3bbb0758d41a0cfe05b60c314460297eae4185f20cad673ceb6dfa05ddbe0278d5de5d")); |