diff options
38 files changed, 1419 insertions, 706 deletions
@@ -143,6 +143,11 @@ <artifactId>velocity</artifactId> <version>1.7</version> </dependency> + <dependency> + <groupId>org.mnode.ical4j</groupId> + <artifactId>ical4j</artifactId> + <version>2.0.0</version> + </dependency> </dependencies> <build> diff --git a/schema/changelog-3.10.xml b/schema/changelog-3.10.xml new file mode 100644 index 000000000..8cd7b3704 --- /dev/null +++ b/schema/changelog-3.10.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<databaseChangeLog + xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog + http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd" + logicalFilePath="changelog-3.10"> + + <changeSet author="author" id="changelog-3.10"> + + <createTable tableName="calendars"> + <column name="id" type="INT" autoIncrement="true"> + <constraints primaryKey="true" /> + </column> + <column name="name" type="VARCHAR(128)"> + <constraints nullable="false" /> + </column> + <column name="calendardata" type="BLOB"> + <constraints nullable="false" /> + </column> + <column name="attributes" type="VARCHAR(4000)"> + <constraints nullable="false" /> + </column> + </createTable> + + <createTable tableName="user_calendar"> + <column name="userid" type="INT"> + <constraints nullable="false" /> + </column> + <column name="calendarid" type="INT"> + <constraints nullable="false" /> + </column> + </createTable> + + <addForeignKeyConstraint baseTableName="user_calendar" baseColumnNames="userid" constraintName="fk_user_calendar_userid" referencedTableName="users" referencedColumnNames="id" onDelete="CASCADE" /> + <addForeignKeyConstraint baseTableName="user_calendar" baseColumnNames="calendarid" constraintName="fk_user_calendar_geofenceid" referencedTableName="calendars" referencedColumnNames="id" onDelete="CASCADE" /> + + <addColumn tableName="geofences"> + <column name="calendarid" type="INT" /> + </addColumn> + + <addForeignKeyConstraint baseColumnNames="calendarid" baseTableName="geofences" constraintName="fk_geofence_calendar_calendarid" onDelete="SET NULL" onUpdate="RESTRICT" referencedColumnNames="id" referencedTableName="calendars"/> + + </changeSet> +</databaseChangeLog> diff --git a/schema/changelog-master.xml b/schema/changelog-master.xml index 448015568..2aed50e72 100644 --- a/schema/changelog-master.xml +++ b/schema/changelog-master.xml @@ -11,4 +11,5 @@ <include file="changelog-3.7.xml" relativeToChangelogFile="true" /> <include file="changelog-3.8.xml" relativeToChangelogFile="true" /> <include file="changelog-3.9.xml" relativeToChangelogFile="true" /> + <include file="changelog-3.10.xml" relativeToChangelogFile="true" /> </databaseChangeLog> diff --git a/setup/default.xml b/setup/default.xml index ad23d7bd3..aeb8fbe54 100644 --- a/setup/default.xml +++ b/setup/default.xml @@ -206,14 +206,15 @@ </entry> <entry key='database.insertGeofence'> - INSERT INTO geofences (name, description, area, attributes) - VALUES (:name, :description, :area, :attributes) + INSERT INTO geofences (name, description, calendarid, area, attributes) + VALUES (:name, :description, :calendarid, :area, :attributes) </entry> <entry key='database.updateGeofence'> UPDATE geofences SET name = :name, description = :description, + calendarid = :calendarid, area = :area, attributes = :attributes WHERE id = :id @@ -315,6 +316,39 @@ INSERT INTO statistics (captureTime, activeUsers, activeDevices, requests, messagesReceived, messagesStored, attributes) VALUES (:captureTime, :activeUsers, :activeDevices, :requests, :messagesReceived, :messagesStored, :attributes) </entry> + + <entry key='database.selectCalendarsAll'> + SELECT * FROM calendars + </entry> + + <entry key='database.insertCalendar'> + INSERT INTO calendars (name, calendarData, attributes) + VALUES (:name, :calendarData, :attributes) + </entry> + + <entry key='database.updateCalendar'> + UPDATE calendars SET + name = :name, + calendarData = :calendarData, + attributes = :attributes + WHERE id = :id + </entry> + + <entry key='database.deleteCalendar'> + DELETE FROM calendars WHERE id = :id + </entry> + + <entry key='database.selectCalendarPermissions'> + SELECT userId, calendarId FROM user_calendar + </entry> + + <entry key='database.linkCalendar'> + INSERT INTO user_calendar (userId, calendarId) VALUES (:userId, :calendarId) + </entry> + + <entry key='database.unlinkCalendar'> + DELETE FROM user_calendar WHERE userId = :userId AND calendarId = :calendarId + </entry> <!-- PROTOCOL CONFIG --> diff --git a/setup/package.sh b/setup/package.sh index 404c7b86b..d9276731c 100755 --- a/setup/package.sh +++ b/setup/package.sh @@ -142,7 +142,7 @@ fi" } package_universal () { - mkdir -p out/{conf,data,lib,logs,web,schema} + mkdir -p out/{conf,data,lib,logs,web,schema,templates} copy_files diff --git a/src/org/traccar/BaseProtocolDecoder.java b/src/org/traccar/BaseProtocolDecoder.java index ea0905bf2..8748a9be6 100644 --- a/src/org/traccar/BaseProtocolDecoder.java +++ b/src/org/traccar/BaseProtocolDecoder.java @@ -23,6 +23,7 @@ import org.traccar.model.Position; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -165,13 +166,31 @@ public abstract class BaseProtocolDecoder extends ExtendedObjectDecoder { } @Override - protected void onMessageEvent(Channel channel, SocketAddress remoteAddress, Object msg) { + protected void onMessageEvent( + Channel channel, SocketAddress remoteAddress, Object originalMessage, Object decodedMessage) { if (Context.getStatisticsManager() != null) { Context.getStatisticsManager().registerMessageReceived(); } - DeviceSession deviceSession = getDeviceSession(channel, remoteAddress); - if (deviceSession != null) { - Context.getConnectionManager().updateDevice(deviceSession.getDeviceId(), Device.STATUS_ONLINE, new Date()); + Position position = null; + if (decodedMessage != null) { + if (decodedMessage instanceof Position) { + position = (Position) decodedMessage; + } else if (decodedMessage instanceof Collection) { + Collection positions = (Collection) decodedMessage; + if (!positions.isEmpty()) { + position = (Position) positions.iterator().next(); + } + } + } + if (position != null) { + Context.getConnectionManager().updateDevice( + position.getDeviceId(), Device.STATUS_ONLINE, new Date()); + } else { + DeviceSession deviceSession = getDeviceSession(channel, remoteAddress); + if (deviceSession != null) { + Context.getConnectionManager().updateDevice( + deviceSession.getDeviceId(), Device.STATUS_ONLINE, new Date()); + } } } diff --git a/src/org/traccar/Context.java b/src/org/traccar/Context.java index 2b8860187..d859e91bb 100644 --- a/src/org/traccar/Context.java +++ b/src/org/traccar/Context.java @@ -23,6 +23,7 @@ import java.util.Properties; import org.apache.velocity.app.VelocityEngine; import org.eclipse.jetty.util.URIUtil; import org.traccar.database.AliasesManager; +import org.traccar.database.CalendarManager; import org.traccar.database.ConnectionManager; import org.traccar.database.DataManager; import org.traccar.database.DeviceManager; @@ -124,6 +125,12 @@ public final class Context { return geofenceManager; } + private static CalendarManager calendarManager; + + public static CalendarManager getCalendarManager() { + return calendarManager; + } + private static NotificationManager notificationManager; public static NotificationManager getNotificationManager() { @@ -235,7 +242,11 @@ public final class Context { switch (type) { case "mozilla": - locationProvider = new MozillaLocationProvider(); + if (key != null) { + locationProvider = new MozillaLocationProvider(key); + } else { + locationProvider = new MozillaLocationProvider(); + } break; default: locationProvider = new OpenCellIdLocationProvider(key); @@ -253,6 +264,7 @@ public final class Context { if (config.getBoolean("event.geofenceHandler")) { geofenceManager = new GeofenceManager(dataManager); + calendarManager = new CalendarManager(dataManager); } if (config.getBoolean("event.enable")) { diff --git a/src/org/traccar/ExtendedObjectDecoder.java b/src/org/traccar/ExtendedObjectDecoder.java index ec03afa60..268e6f688 100644 --- a/src/org/traccar/ExtendedObjectDecoder.java +++ b/src/org/traccar/ExtendedObjectDecoder.java @@ -56,7 +56,7 @@ public abstract class ExtendedObjectDecoder implements ChannelUpstreamHandler { MessageEvent e = (MessageEvent) evt; Object originalMessage = e.getMessage(); Object decodedMessage = decode(e.getChannel(), e.getRemoteAddress(), originalMessage); - onMessageEvent(e.getChannel(), e.getRemoteAddress(), originalMessage); // call after decode + onMessageEvent(e.getChannel(), e.getRemoteAddress(), originalMessage, decodedMessage); if (originalMessage == decodedMessage) { ctx.sendUpstream(evt); } else { @@ -77,7 +77,8 @@ public abstract class ExtendedObjectDecoder implements ChannelUpstreamHandler { } } - protected void onMessageEvent(Channel channel, SocketAddress remoteAddress, Object msg) { + protected void onMessageEvent( + Channel channel, SocketAddress remoteAddress, Object originalMessage, Object decodedMessage) { } protected Object handleEmptyMessage(Channel channel, SocketAddress remoteAddress, Object msg) { diff --git a/src/org/traccar/api/resource/CalendarPermissionResource.java b/src/org/traccar/api/resource/CalendarPermissionResource.java new file mode 100644 index 000000000..a49254b6b --- /dev/null +++ b/src/org/traccar/api/resource/CalendarPermissionResource.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2016 Andrey Kunitsyn (andrey@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.api.resource; + +import java.sql.SQLException; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.traccar.Context; +import org.traccar.api.BaseResource; +import org.traccar.model.CalendarPermission; + +@Path("permissions/calendars") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class CalendarPermissionResource extends BaseResource { + + @POST + public Response add(CalendarPermission entity) throws SQLException { + Context.getPermissionsManager().checkReadonly(getUserId()); + Context.getPermissionsManager().checkUser(getUserId(), entity.getUserId()); + Context.getPermissionsManager().checkCalendar(getUserId(), entity.getCalendarId()); + Context.getDataManager().linkCalendar(entity.getUserId(), entity.getCalendarId()); + Context.getCalendarManager().refreshUserCalendars(); + return Response.ok(entity).build(); + } + + @DELETE + public Response remove(CalendarPermission entity) throws SQLException { + Context.getPermissionsManager().checkReadonly(getUserId()); + Context.getPermissionsManager().checkUser(getUserId(), entity.getUserId()); + Context.getPermissionsManager().checkCalendar(getUserId(), entity.getCalendarId()); + Context.getDataManager().unlinkCalendar(entity.getUserId(), entity.getCalendarId()); + Context.getCalendarManager().refreshUserCalendars(); + return Response.noContent().build(); + } +} diff --git a/src/org/traccar/api/resource/CalendarResource.java b/src/org/traccar/api/resource/CalendarResource.java new file mode 100644 index 000000000..0a9bb5daf --- /dev/null +++ b/src/org/traccar/api/resource/CalendarResource.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2016 Andrey Kunitsyn (andrey@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.api.resource; + +import java.sql.SQLException; +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.traccar.Context; +import org.traccar.api.BaseResource; +import org.traccar.model.Calendar; + +@Path("calendars") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class CalendarResource extends BaseResource { + + @GET + public Collection<Calendar> get( + @QueryParam("all") boolean all, @QueryParam("userId") long userId) throws SQLException { + + if (all) { + Context.getPermissionsManager().checkAdmin(getUserId()); + return Context.getCalendarManager().getAllCalendars(); + } else { + if (userId == 0) { + userId = getUserId(); + } + Context.getPermissionsManager().checkUser(getUserId(), userId); + return Context.getCalendarManager().getUserCalendars(userId); + } + } + + @POST + public Response add(Calendar entity) throws SQLException { + Context.getPermissionsManager().checkReadonly(getUserId()); + Context.getCalendarManager().addCalendar(entity); + Context.getDataManager().linkCalendar(getUserId(), entity.getId()); + Context.getCalendarManager().refreshUserCalendars(); + return Response.ok(entity).build(); + } + + @Path("{id}") + @PUT + public Response update(Calendar entity) throws SQLException { + Context.getPermissionsManager().checkReadonly(getUserId()); + Context.getPermissionsManager().checkCalendar(getUserId(), entity.getId()); + Context.getCalendarManager().updateCalendar(entity); + return Response.ok(entity).build(); + } + + @Path("{id}") + @DELETE + public Response remove(@PathParam("id") long id) throws SQLException { + Context.getPermissionsManager().checkReadonly(getUserId()); + Context.getPermissionsManager().checkCalendar(getUserId(), id); + Context.getCalendarManager().removeCalendar(id); + return Response.noContent().build(); + } +} diff --git a/src/org/traccar/database/CalendarManager.java b/src/org/traccar/database/CalendarManager.java new file mode 100644 index 000000000..3e95f6698 --- /dev/null +++ b/src/org/traccar/database/CalendarManager.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2016 Andrey Kunitsyn (andrey@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.database; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.traccar.helper.Log; +import org.traccar.model.Calendar; +import org.traccar.model.CalendarPermission; + +public class CalendarManager { + + private final DataManager dataManager; + + private final Map<Long, Calendar> calendars = new ConcurrentHashMap<>(); + private final Map<Long, Set<Long>> userCalendars = new ConcurrentHashMap<>(); + + public CalendarManager(DataManager dataManager) { + this.dataManager = dataManager; + refreshCalendars(); + } + + public final void refreshCalendars() { + if (dataManager != null) { + try { + calendars.clear(); + for (Calendar calendar : dataManager.getCalendars()) { + calendars.put(calendar.getId(), calendar); + } + } catch (SQLException error) { + Log.warning(error); + } + } + refreshUserCalendars(); + } + + private Set<Long> getUserCalendarIds(long userId) { + if (!userCalendars.containsKey(userId)) { + userCalendars.put(userId, new HashSet<Long>()); + } + return userCalendars.get(userId); + } + + public Collection<Calendar> getUserCalendars(long userId) { + ArrayList<Calendar> result = new ArrayList<>(); + for (long calendarId : getUserCalendarIds(userId)) { + result.add(calendars.get(calendarId)); + } + return result; + } + + public final void refreshUserCalendars() { + if (dataManager != null) { + try { + userCalendars.clear(); + for (CalendarPermission calendarsPermission : dataManager.getCalendarPermissions()) { + getUserCalendarIds(calendarsPermission.getUserId()).add(calendarsPermission.getCalendarId()); + } + } catch (SQLException error) { + Log.warning(error); + } + } + } + + public Calendar getCalendar(long calendarId) { + return calendars.get(calendarId); + } + + public final void addCalendar(Calendar calendar) throws SQLException { + dataManager.addCalendar(calendar); + calendars.put(calendar.getId(), calendar); + } + + public final void updateCalendar(Calendar calendar) throws SQLException { + dataManager.updateCalendar(calendar); + calendars.put(calendar.getId(), calendar); + } + + public final void removeCalendar(long calendarId) throws SQLException { + dataManager.removeCalendar(calendarId); + calendars.remove(calendarId); + refreshUserCalendars(); + } + + public Collection<Calendar> getAllCalendars() { + return calendars.values(); + } + + public boolean checkCalendar(long userId, long calendarId) { + return getUserCalendarIds(userId).contains(calendarId); + } +} diff --git a/src/org/traccar/database/DataManager.java b/src/org/traccar/database/DataManager.java index 8be53ad7b..278109229 100644 --- a/src/org/traccar/database/DataManager.java +++ b/src/org/traccar/database/DataManager.java @@ -37,6 +37,8 @@ import liquibase.resource.ResourceAccessor; import org.traccar.Config; import org.traccar.helper.Log; import org.traccar.model.AttributeAlias; +import org.traccar.model.Calendar; +import org.traccar.model.CalendarPermission; import org.traccar.model.Device; import org.traccar.model.DevicePermission; import org.traccar.model.Event; @@ -484,4 +486,45 @@ public class DataManager { .executeUpdate()); } + public Collection<Calendar> getCalendars() throws SQLException { + return QueryBuilder.create(dataSource, getQuery("database.selectCalendarsAll")) + .executeQuery(Calendar.class); + } + + public void addCalendar(Calendar calendar) throws SQLException { + calendar.setId(QueryBuilder.create(dataSource, getQuery("database.insertCalendar"), true) + .setObject(calendar) + .executeUpdate()); + } + + public void updateCalendar(Calendar calendar) throws SQLException { + QueryBuilder.create(dataSource, getQuery("database.updateCalendar")) + .setObject(calendar) + .executeUpdate(); + } + + public void removeCalendar(long calendarId) throws SQLException { + QueryBuilder.create(dataSource, getQuery("database.deleteCalendar")) + .setLong("id", calendarId) + .executeUpdate(); + } + + public Collection<CalendarPermission> getCalendarPermissions() throws SQLException { + return QueryBuilder.create(dataSource, getQuery("database.selectCalendarPermissions")) + .executeQuery(CalendarPermission.class); + } + + public void linkCalendar(long userId, long calendarId) throws SQLException { + QueryBuilder.create(dataSource, getQuery("database.linkCalendar")) + .setLong("userId", userId) + .setLong("calendarId", calendarId) + .executeUpdate(); + } + + public void unlinkCalendar(long userId, long calendarId) throws SQLException { + QueryBuilder.create(dataSource, getQuery("database.unlinkCalendar")) + .setLong("userId", userId) + .setLong("calendarId", calendarId) + .executeUpdate(); + } } diff --git a/src/org/traccar/database/PermissionsManager.java b/src/org/traccar/database/PermissionsManager.java index 078a5f935..6c0610655 100644 --- a/src/org/traccar/database/PermissionsManager.java +++ b/src/org/traccar/database/PermissionsManager.java @@ -207,6 +207,12 @@ public class PermissionsManager { } } + public void checkCalendar(long userId, long calendarId) throws SecurityException { + if (!Context.getCalendarManager().checkCalendar(userId, calendarId) && !isAdmin(userId)) { + throw new SecurityException("Calendar access denied"); + } + } + public Server getServer() { return server; } diff --git a/src/org/traccar/database/QueryBuilder.java b/src/org/traccar/database/QueryBuilder.java index 50d689a2a..201240f2f 100644 --- a/src/org/traccar/database/QueryBuilder.java +++ b/src/org/traccar/database/QueryBuilder.java @@ -240,6 +240,23 @@ public final class QueryBuilder { return this; } + public QueryBuilder setBlob(String name, byte[] value) throws SQLException { + for (int i : indexes(name)) { + try { + if (value == null) { + statement.setNull(i, Types.BLOB); + } else { + statement.setBytes(i, value); + } + } catch (SQLException error) { + statement.close(); + connection.close(); + throw error; + } + } + return this; + } + public QueryBuilder setObject(Object object) throws SQLException { Method[] methods = object.getClass().getMethods(); @@ -260,6 +277,8 @@ public final class QueryBuilder { setString(name, (String) method.invoke(object)); } else if (method.getReturnType().equals(Date.class)) { setDate(name, (Date) method.invoke(object)); + } else if (method.getReturnType().equals(byte[].class)) { + setBlob(name, (byte[]) method.invoke(object)); } else if (method.getReturnType().equals(Map.class)) { if (Context.getConfig().getBoolean("database.xml")) { setString(name, MiscFormatter.toXmlString((Map) method.invoke(object))); @@ -375,6 +394,17 @@ public final class QueryBuilder { } } }); + } else if (parameterType.equals(byte[].class)) { + processors.add(new ResultSetProcessor<T>() { + @Override + public void process(T object, ResultSet resultSet) throws SQLException { + try { + method.invoke(object, resultSet.getBytes(name)); + } catch (IllegalAccessException | InvocationTargetException error) { + Log.warning(error); + } + } + }); } } diff --git a/src/org/traccar/events/GeofenceEventHandler.java b/src/org/traccar/events/GeofenceEventHandler.java index d31e516ef..fbec932b1 100644 --- a/src/org/traccar/events/GeofenceEventHandler.java +++ b/src/org/traccar/events/GeofenceEventHandler.java @@ -57,14 +57,22 @@ public class GeofenceEventHandler extends BaseEventHandler { Collection<Event> events = new ArrayList<>(); for (long geofenceId : newGeofences) { - Event event = new Event(Event.TYPE_GEOFENCE_ENTER, position.getDeviceId(), position.getId()); - event.setGeofenceId(geofenceId); - events.add(event); + long calendarId = geofenceManager.getGeofence(geofenceId).getCalendarId(); + if (calendarId == 0 || Context.getCalendarManager().getCalendar(calendarId) == null + || Context.getCalendarManager().getCalendar(calendarId).checkMoment(position.getFixTime())) { + Event event = new Event(Event.TYPE_GEOFENCE_ENTER, position.getDeviceId(), position.getId()); + event.setGeofenceId(geofenceId); + events.add(event); + } } for (long geofenceId : oldGeofences) { - Event event = new Event(Event.TYPE_GEOFENCE_EXIT, position.getDeviceId(), position.getId()); - event.setGeofenceId(geofenceId); - events.add(event); + long calendarId = geofenceManager.getGeofence(geofenceId).getCalendarId(); + if (calendarId == 0 || Context.getCalendarManager().getCalendar(calendarId) == null + || Context.getCalendarManager().getCalendar(calendarId).checkMoment(position.getFixTime())) { + Event event = new Event(Event.TYPE_GEOFENCE_EXIT, position.getDeviceId(), position.getId()); + event.setGeofenceId(geofenceId); + events.add(event); + } } return events; } diff --git a/src/org/traccar/location/MozillaLocationProvider.java b/src/org/traccar/location/MozillaLocationProvider.java index 37040a95e..cbfc19550 100644 --- a/src/org/traccar/location/MozillaLocationProvider.java +++ b/src/org/traccar/location/MozillaLocationProvider.java @@ -25,15 +25,17 @@ import javax.json.JsonReader; public class MozillaLocationProvider extends BaseLocationProvider { + private static final String URL = "https://location.services.mozilla.com/v1/geolocate"; + private String url; private String template; public MozillaLocationProvider() { - this("https://location.services.mozilla.com/v1/geolocate", "test"); + this("test"); } - public MozillaLocationProvider(String url, String key) { - this.url = url + "?key=" + key; + public MozillaLocationProvider(String key) { + this.url = URL + "?key=" + key; template = new StringBuilder() .append("{\"cellTowers\":[{") diff --git a/src/org/traccar/model/Calendar.java b/src/org/traccar/model/Calendar.java new file mode 100644 index 000000000..19b5fde16 --- /dev/null +++ b/src/org/traccar/model/Calendar.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2016 Andrey Kunitsyn (andrey@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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Date; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.filter.Filter; +import net.fortuna.ical4j.filter.PeriodRule; +import net.fortuna.ical4j.filter.Rule; +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.DateTime; +import net.fortuna.ical4j.model.Dur; +import net.fortuna.ical4j.model.Period; +import net.fortuna.ical4j.model.component.CalendarComponent; +import net.fortuna.ical4j.validate.ValidationException; + +public class Calendar extends Extensible { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + private byte[] calendarData; + + public byte[] getCalendarData() throws ValidationException, IOException { + return calendarData.clone(); + } + + public void setCalendarData(byte[] calendarData) throws IOException, ParserException, SQLException { + CalendarBuilder builder = new CalendarBuilder(); + calendar = builder.build(new ByteArrayInputStream(calendarData)); + this.calendarData = calendarData.clone(); + } + + private net.fortuna.ical4j.model.Calendar calendar; + + @JsonIgnore + public net.fortuna.ical4j.model.Calendar getCalendar() { + return calendar; + } + + public boolean checkMoment(Date date) { + if (calendar != null) { + Period period = new Period(new DateTime(date), new Dur(0, 0, 0, 0)); + Rule<Component> periodRule = new PeriodRule<Component>(period); + Filter<CalendarComponent> filter = new Filter<CalendarComponent>(new Rule[] {periodRule}, Filter.MATCH_ANY); + Collection<CalendarComponent> events = filter.filter(calendar.getComponents(Component.VEVENT)); + if (events != null && !events.isEmpty()) { + return true; + } + } + return false; + } +} diff --git a/src/org/traccar/model/CalendarPermission.java b/src/org/traccar/model/CalendarPermission.java new file mode 100644 index 000000000..59f54e07b --- /dev/null +++ b/src/org/traccar/model/CalendarPermission.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Anton Tananaev (anton@traccar.org) + * Copyright 2016 Andrey Kunitsyn (andrey@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; + +public class CalendarPermission { + + private long userId; + + public long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + private long calendarId; + + public long getCalendarId() { + return calendarId; + } + + public void setCalendarId(long calendarId) { + this.calendarId = calendarId; + } +} diff --git a/src/org/traccar/model/Geofence.java b/src/org/traccar/model/Geofence.java index 326c45b5f..f10ce6862 100644 --- a/src/org/traccar/model/Geofence.java +++ b/src/org/traccar/model/Geofence.java @@ -84,4 +84,13 @@ public class Geofence extends Extensible { this.geometry = geometry; } + private long calendarId; + + public long getCalendarId() { + return calendarId; + } + + public void setCalendarId(long calendarId) { + this.calendarId = calendarId; + } } diff --git a/src/org/traccar/protocol/AplicomProtocolDecoder.java b/src/org/traccar/protocol/AplicomProtocolDecoder.java index 8c06aad6f..bcaac5349 100644 --- a/src/org/traccar/protocol/AplicomProtocolDecoder.java +++ b/src/org/traccar/protocol/AplicomProtocolDecoder.java @@ -466,7 +466,10 @@ public class AplicomProtocolDecoder extends BaseProtocolDecoder { position.set("tripDistance", buf.readUnsignedInt() * 5); position.set("serviceDistance", (buf.readUnsignedInt() - 2105540607) * 5); } else if (type == 0x0A) { - position.set("brakeData", ChannelBuffers.hexDump(buf.readBytes(length))); + ChannelBuffer brakeData = buf.readBytes(length); + position.set("absStatusCounter", brakeData.readUnsignedShort()); + position.set("atvbStatusCounter", brakeData.readUnsignedShort()); + position.set("vdcActiveCounter", brakeData.readUnsignedShort()); } else if (type == 0x0B) { position.set("brakeMinMaxData", ChannelBuffers.hexDump(buf.readBytes(length))); } else if (type == 0x0C) { diff --git a/src/org/traccar/protocol/AutoGradeProtocolDecoder.java b/src/org/traccar/protocol/AutoGradeProtocolDecoder.java index d8fe8b1a2..744117cfe 100644 --- a/src/org/traccar/protocol/AutoGradeProtocolDecoder.java +++ b/src/org/traccar/protocol/AutoGradeProtocolDecoder.java @@ -89,7 +89,7 @@ public class AutoGradeProtocolDecoder extends BaseProtocolDecoder { position.setCourse(parser.nextDouble()); - int status = (byte) parser.next().charAt(0); + int status = parser.next().charAt(0); position.set(Position.KEY_STATUS, status); position.set(Position.KEY_IGNITION, BitUtil.check(status, 0)); diff --git a/src/org/traccar/web/WebServer.java b/src/org/traccar/web/WebServer.java index 8201f8d16..4dd37f4cc 100644 --- a/src/org/traccar/web/WebServer.java +++ b/src/org/traccar/web/WebServer.java @@ -102,7 +102,7 @@ public class WebServer { ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setResourceBase(config.getString("web.path")); if (config.getBoolean("web.debug")) { - resourceHandler.setWelcomeFiles(new String[] {"debug.html"}); + resourceHandler.setWelcomeFiles(new String[] {"debug.html", "index.html"}); resourceHandler.setMinMemoryMappedContentLength(-1); // avoid locking files on Windows } else { resourceHandler.setWelcomeFiles(new String[] {"release.html", "index.html"}); diff --git a/swagger.json b/swagger.json index 25b76963c..0f298f161 100644 --- a/swagger.json +++ b/swagger.json @@ -1,23 +1,29 @@ { "swagger": "2.0", "info": { - "version": "3.8", + "version": "3.9", "title": "traccar" }, - "host": "traccar.org", + "host": "demo.traccar.org", "basePath": "/api", "schemes": [ "http" ], + "security": [ + { + "basicAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], "paths": { "/commands": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Dispatch commands to device", "parameters": [ { "name": "body", @@ -31,70 +37,53 @@ "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Command" } + }, + "400": { + "description": "Could happen when dispatching to a device that is offline, the user doesn't have permission or an incorrect command _type_ for the device" } } } }, "/devices": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of Devices", + "description": "Without any params, returns a list of the user's devices", "parameters": [ { - "name": "all", - "in": "query", - "required": true, - "type": "boolean" + "$ref": "#/parameters/all" }, { - "name": "userId", - "in": "query", - "required": true, - "type": "integer" + "$ref": "#/parameters/userId" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { "$ref": "#/definitions/Device" } } + }, + "400": { + "description": "No permission" } } }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Create a Device", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Device" - } + "$ref": "#/parameters/Device" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Device" } @@ -104,32 +93,18 @@ }, "/devices/{id}": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update a Device", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" }, { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Device" - } + "$ref": "#/parameters/Device" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Device" } @@ -137,42 +112,25 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update a Device", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/devices/{id}/distance": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update the distance counter of the Device", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" }, { "name": "body", @@ -185,34 +143,22 @@ ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/devices/geofences": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Link a Geofence to a Device", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/DeviceGeofence" - } + "$ref": "#/parameters/DeviceGeofence" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/DeviceGeofence" } @@ -220,56 +166,34 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Remove a Geofence from a Device", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/DeviceGeofence" - } + "$ref": "#/parameters/DeviceGeofence" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/groups": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of Groups", + "description": "Without any params, returns a list of the Groups the user belongs to", "parameters": [ { - "name": "all", - "in": "query", - "required": true, - "type": "boolean" + "$ref": "#/parameters/all" }, { - "name": "userId", - "in": "query", - "required": true, - "type": "integer" + "$ref": "#/parameters/userId" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -280,61 +204,39 @@ } }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Create a Group", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Group" - } + "$ref": "#/parameters/Group" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Group" } + }, + "400": { + "description": "No permission" } } } }, "/groups/{id}": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update a Group", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" }, { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Group" - } + "$ref": "#/parameters/Group" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Group" } @@ -342,50 +244,30 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Delete a Group", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/groups/geofences": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Link a Geofence to a Group", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupGeofence" - } + "$ref": "#/parameters/GroupGeofence" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/GroupGeofence" } @@ -393,105 +275,64 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Remove a Geofence from a Group", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupGeofence" - } + "$ref": "#/parameters/GroupGeofence" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/permissions/devices": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Link a Device to a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/DevicePermission" - } + "$ref": "#/parameters/DevicePermission" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/DevicePermission" } + }, + "400": { + "description": "No permission" } } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Remove a Device from a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/DevicePermission" - } + "$ref": "#/parameters/DevicePermission" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/permissions/groups": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Link a Group to a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupPermission" - } + "$ref": "#/parameters/GroupPermission" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/GroupPermission" } @@ -499,52 +340,30 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Remove a Group from a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GroupPermission" - } + "$ref": "#/parameters/GroupPermission" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/permissions/geofences": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Link a Geofence to a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GeofencePermission" - } + "$ref": "#/parameters/GeofencePermission" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/GeofencePermission" } @@ -552,26 +371,15 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Remove a Geofence from a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/GeofencePermission" - } + "$ref": "#/parameters/GeofencePermission" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } @@ -579,13 +387,7 @@ "/positions": { "get": { "summary" : "Fetches a list of Positions", - "description" : "Without any params, it returns a list of last known positions for all the user's devices", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "description" : "Without any params, it returns a list of last known positions for all the user's Devices. _from_ and _to_ fields are not required with _id_", "parameters": [ { "name": "deviceId", @@ -597,17 +399,20 @@ { "name": "from", "in": "query", - "description": "Not required with _id_", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`", "required": false, - "type": "string" + "type": "string", + "format": "date-time" }, { "name": "to", "in": "query", - "description": "Not required with _id_", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`", "required": false, - "type": "string" - }, { + "type": "string", + "format": "date-time" + }, + { "name" : "id", "in" : "query", "description" : "To fetch one or more positions. Multiple params can be passed like `id=31&id=42`", @@ -631,17 +436,10 @@ }, "/server": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [], + "summary": "Fetch Server information", "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Server" } @@ -649,12 +447,7 @@ } }, "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update Server information", "parameters": [ { "name": "body", @@ -668,7 +461,6 @@ "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Server" } @@ -678,34 +470,27 @@ }, "/session": { "get": { + "summary": "Fetch Session information", "consumes": [ "application/x-www-form-urlencoded" ], - "produces": [ - "application/json" - ], - "parameters": [], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/User" } }, "404": { - "description": "Not Found", - "headers": {} + "description": "Not Found" } } }, "post": { + "summary": "Create a new Session", "consumes": [ "application/x-www-form-urlencoded" ], - "produces": [ - "application/json" - ], "parameters": [ { "name": "email", @@ -717,82 +502,63 @@ "name": "password", "in": "formData", "required": true, - "type": "string" + "type": "string", + "format": "password" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/User" } }, "401": { - "description": "Unauthorized", - "headers": {} + "description": "Unauthorized" } } }, "delete": { + "summary": "Close the Session", "consumes": [ "application/x-www-form-urlencoded" ], - "produces": [ - "application/json" - ], "parameters": [], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/users": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [], + "summary": "Fetch a list of Users", "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { "$ref": "#/definitions/User" } } + }, + "400": { + "description": "No Permission" } } }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Create a User", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/User" - } + "$ref": "#/parameters/User" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/User" } @@ -802,32 +568,18 @@ }, "/users/{id}": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update a User", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" }, { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/User" - } + "$ref": "#/parameters/User" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/User" } @@ -835,54 +587,37 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Delete a User", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/users/notifications": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of Notification types", + "description": "Without params, it returns a list of the user's enabled Notifications", "parameters": [ { "name": "all", "in": "query", - "required": true, + "description": "To fetch a list of all available Notifications", "type": "boolean" }, { - "name": "userId", - "in": "query", - "required": true, - "type": "integer" + "$ref": "#/parameters/userId" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -893,12 +628,7 @@ } }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Set or unset a Notification", "parameters": [ { "name": "body", @@ -912,7 +642,6 @@ "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Notification" } @@ -922,12 +651,7 @@ }, "/commandtypes": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of available Commands for the Device", "parameters": [ { "name": "deviceId", @@ -939,49 +663,37 @@ "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { "$ref": "#/definitions/CommandType" } } + }, + "400": { + "description": "Could happen when trying to fetch from an offline device or the user does not have permission" } } } }, "/geofences": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of Geofences", + "description": "Without params, it returns a list of Geofences the user has access to", "parameters": [ { - "name": "all", - "in": "query", - "required": true, - "type": "boolean" + "$ref": "#/parameters/all" }, { - "name": "userId", - "in": "query", - "required": true, - "type": "integer" + "$ref": "#/parameters/userId" }, { "name": "groupId", "in": "query", - "required": true, "type": "integer" }, { - "name": "deviceId", - "in": "query", - "required": true, - "type": "integer" + "$ref": "#/parameters/deviceId" }, { "name": "refresh", @@ -993,7 +705,6 @@ "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1004,26 +715,15 @@ } }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Create a Geofence", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Geofence" - } + "$ref": "#/parameters/Geofence" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Geofence" } @@ -1033,32 +733,18 @@ }, "/geofences/{id}": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update a Geofence", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" }, { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/Geofence" - } + "$ref": "#/parameters/Geofence" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Geofence" } @@ -1066,48 +752,29 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Delete a Geofence", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } }, "/events/{id}": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/Event" } @@ -1117,50 +784,25 @@ }, "/reports/route": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of Positions within the time period for the Devices or Groups", + "description": "At least one _deviceId_ or one _groupId_ must be passed", "parameters": [ { - "name": "deviceId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/deviceIdArray" }, { - "name": "groupId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/groupIdArray" }, { - "name": "from", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/fromTime" }, { - "name": "to", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/toTime" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1173,60 +815,34 @@ }, "/reports/events": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of Events within the time period for the Devices or Groups", + "description": "At least one _deviceId_ or one _groupId_ must be passed", "parameters": [ { - "name": "deviceId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/deviceIdArray" }, { - "name": "groupId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/groupIdArray" }, { "name": "type", "in": "query", "description": "% can be used to return events of all types", - "required": true, "type": "array", "items": { "type": "string" } }, { - "name": "from", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/fromTime" }, { - "name": "to", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/toTime" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1239,50 +855,25 @@ }, "/reports/summary": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of ReportSummary within the time period for the Devices or Groups", + "description": "At least one _deviceId_ or one _groupId_ must be passed", "parameters": [ { - "name": "deviceId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/deviceIdArray" }, { - "name": "groupId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/groupIdArray" }, { - "name": "from", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/fromTime" }, { - "name": "to", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/toTime" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1295,50 +886,25 @@ }, "/reports/trips": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of ReportTrips within the time period for the Devices or Groups", + "description": "At least one _deviceId_ or one _groupId_ must be passed", "parameters": [ { - "name": "deviceId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/deviceIdArray" }, { - "name": "groupId", - "in": "query", - "description": "at least one deviceId or one groupId must be passed", - "required": true, - "type": "array", - "items": { - "type": "integer" - } + "$ref": "#/parameters/groupIdArray" }, { - "name": "from", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/fromTime" }, { - "name": "to", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/toTime" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1351,30 +917,18 @@ }, "/statistics": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch server Statistics", "parameters": [ { - "name": "from", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/fromTime" }, { - "name": "to", - "in": "query", - "required": true, - "type": "string" + "$ref": "#/parameters/toTime" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1387,24 +941,16 @@ }, "/attributes/aliases": { "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Fetch a list of AttributeAlias", + "description": "Without params, it returns a list of AttributeAlias from all the user's Devices", "parameters": [ { - "name": "deviceId", - "in": "query", - "required": false, - "type": "integer" + "$ref": "#/parameters/deviceId" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "type": "array", "items": { @@ -1415,26 +961,15 @@ } }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Set an AttributeAlias", "parameters": [ { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/AttributeAlias" - } + "$ref": "#/parameters/AttributeAlias" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/AttributeAlias" } @@ -1444,32 +979,18 @@ }, "/attributes/aliases/{id}": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Update an AttributeAlias", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" }, { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/AttributeAlias" - } + "$ref": "#/parameters/AttributeAlias" } ], "responses": { "200": { "description": "OK", - "headers": {}, "schema": { "$ref": "#/definitions/AttributeAlias" } @@ -1477,24 +998,15 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], + "summary": "Delete an AttributeAlias", "parameters": [ { - "name": "id", - "in": "path", - "required": true, - "type": "integer" + "$ref": "#/parameters/entityId" } ], "responses": { "204": { - "description": "No Content", - "headers": {} + "description": "No Content" } } } @@ -1513,10 +1025,19 @@ "type": "string" }, "deviceTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "fixTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" + }, + "serverTime": { + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "outdated": { "type": "boolean" @@ -1534,7 +1055,8 @@ "type": "number" }, "speed": { - "type": "number" + "type": "number", + "description": "in knots" }, "course": { "type": "number" @@ -1593,7 +1115,9 @@ "type": "boolean" }, "expirationTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "deviceLimit": { "type": "integer" @@ -1680,7 +1204,9 @@ "type": "string" }, "lastUpdate": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "positionId": { "type": "integer" @@ -1700,7 +1226,12 @@ "category": { "type": "string" }, - "geofenceIds": {}, + "geofenceIds": { + "type": "array", + "items": { + "type": "integer" + } + }, "attributes": {} } }, @@ -1815,7 +1346,9 @@ "type": "string" }, "serverTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "deviceId": { "type": "integer" @@ -1838,13 +1371,16 @@ "type": "string" }, "maxSpeed": { - "type": "number" + "type": "number", + "description": "in knots" }, "averageSpeed": { - "type": "number" + "type": "number", + "description": "in knots" }, "distance": { - "type": "number" + "type": "number", + "description": "in meters" }, "engineHours": { "type": "integer" @@ -1860,19 +1396,24 @@ "type": "string" }, "maxSpeed": { - "type": "number" + "type": "number", + "description": "in knots" }, "averageSpeed": { - "type": "number" + "type": "number", + "description": "in knots" }, "distance": { - "type": "number" + "type": "number", + "description": "in meters" }, "duration": { "type": "integer" }, "startTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "startAddress": { "type": "string" @@ -1884,7 +1425,9 @@ "type": "number" }, "endTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "endAddress": { "type": "string" @@ -1900,7 +1443,9 @@ "Statistics": { "properties": { "captureTime": { - "type": "string" + "type": "string", + "format": "date-time", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`" }, "activeUsers": { "type": "integer" @@ -1941,9 +1486,156 @@ "type": "integer" }, "totalDistance": { - "type": "number" + "type": "number", + "description": "in meters" } } } + }, + "parameters": { + "entityId": { + "name": "id", + "in": "path", + "required": true, + "type": "integer" + }, + "all": { + "name": "all", + "in": "query", + "description": "Can only be used by admin users to fetch all entities", + "type": "boolean" + }, + "userId": { + "name": "userId", + "in": "query", + "description": "Standard users can use this only with their own _userId_", + "type": "integer" + }, + "deviceId": { + "name": "deviceId", + "in": "query", + "description": "Standard users can use this only with _userId_s, they have access to", + "type": "integer" + }, + "Device": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Device" + } + }, + "DeviceGeofence": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeviceGeofence" + } + }, + "Group": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Group" + } + }, + "GroupGeofence": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupGeofence" + } + }, + "DevicePermission": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DevicePermission" + } + }, + "GroupPermission": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GroupPermission" + } + }, + "GeofencePermission": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GeofencePermission" + } + }, + "User": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + }, + "Geofence": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Geofence" + } + }, + "AttributeAlias": { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AttributeAlias" + } + }, + "deviceIdArray": { + "name": "deviceId", + "in": "query", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi" + }, + "groupIdArray": { + "name": "groupId", + "in": "query", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi" + }, + "fromTime": { + "name": "from", + "in": "query", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`", + "required": true, + "type": "string", + "format": "date-time" + }, + "toTime": { + "name": "to", + "in": "query", + "description": "in IS0 8601 format. eg. `1963-11-22T18:30:00Z`", + "required": true, + "type": "string", + "format": "date-time" + } + }, + "securityDefinitions": { + "basicAuth": { + "type": "basic", + "description": "Basic HTTP authorization with _email_ and _password_" + } } } diff --git a/templates/mail/alarm.vm b/templates/mail/alarm.vm index 1d2b4fdf8..b64b2126a 100644 --- a/templates/mail/alarm.vm +++ b/templates/mail/alarm.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Alarm: $position.getString("alarm")<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/deviceMoving.vm b/templates/mail/deviceMoving.vm index a946753e4..9ad2d8bdc 100644 --- a/templates/mail/deviceMoving.vm +++ b/templates/mail/deviceMoving.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Moving<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/deviceOverspeed.vm b/templates/mail/deviceOverspeed.vm index 7b99c6a06..a8a58a181 100644 --- a/templates/mail/deviceOverspeed.vm +++ b/templates/mail/deviceOverspeed.vm @@ -12,6 +12,6 @@ Device: $device.name<br> Exceeds the speed: $speedString<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/deviceStopped.vm b/templates/mail/deviceStopped.vm index c36e6f1b6..273e1c988 100644 --- a/templates/mail/deviceStopped.vm +++ b/templates/mail/deviceStopped.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Stopped<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/geofenceEnter.vm b/templates/mail/geofenceEnter.vm index cef24723a..75d16617f 100644 --- a/templates/mail/geofenceEnter.vm +++ b/templates/mail/geofenceEnter.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Has entered geofence: $geofence.name<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/geofenceExit.vm b/templates/mail/geofenceExit.vm index e696e6556..6d4d1639d 100644 --- a/templates/mail/geofenceExit.vm +++ b/templates/mail/geofenceExit.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Has exited geofence: $geofence.name<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/ignitionOff.vm b/templates/mail/ignitionOff.vm index 229405cca..3a3212b12 100644 --- a/templates/mail/ignitionOff.vm +++ b/templates/mail/ignitionOff.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Ignition OFF<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/ignitionOn.vm b/templates/mail/ignitionOn.vm index 2aeea0132..bbe6c40f6 100644 --- a/templates/mail/ignitionOn.vm +++ b/templates/mail/ignitionOn.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Ignition ON<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/templates/mail/maintenance.vm b/templates/mail/maintenance.vm index 4184d138f..c94c35cc4 100644 --- a/templates/mail/maintenance.vm +++ b/templates/mail/maintenance.vm @@ -5,6 +5,6 @@ Device: $device.name<br> Maintenance is required<br> Time: $event.serverTime<br> -Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> +Point: <a href="$webUrl?eventId=$event.id">#{if}($position.address)$position.address#{else}$position.latitude°, $position.longitude°#{end}</a><br> </body> </html> diff --git a/test/org/traccar/calendar/CalendarTest.java b/test/org/traccar/calendar/CalendarTest.java new file mode 100644 index 000000000..7f5bd7d29 --- /dev/null +++ b/test/org/traccar/calendar/CalendarTest.java @@ -0,0 +1,58 @@ +package org.traccar.calendar; + +import java.io.IOException; +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; +import org.traccar.model.Calendar; + +import net.fortuna.ical4j.data.ParserException; + +public class CalendarTest { + + @Test + public void testCalendar() throws IOException, ParserException, ParseException, SQLException { + String calendarString = "BEGIN:VCALENDAR\n" + + "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:Asia/Yekaterinburg\n" + + "BEGIN:STANDARD\n" + + "TZOFFSETFROM:+0500\n" + + "TZOFFSETTO:+0500\n" + + "TZNAME:YEKT\n" + + "DTSTART:19700101T000000\n" + + "END:STANDARD\n" + + "END:VTIMEZONE\n" + + "BEGIN:VEVENT\n" + + "CREATED:20161213T045151Z\n" + + "LAST-MODIFIED:20161213T045242Z\n" + + "DTSTAMP:20161213T045242Z\n" + + "UID:9d000df0-6354-479d-a407-218dac62c7c9\n" + + "SUMMARY:Every night\n" + + "RRULE:FREQ=DAILY\n" + + "DTSTART;TZID=Asia/Yekaterinburg:20161130T230000\n" + + "DTEND;TZID=Asia/Yekaterinburg:20161201T070000\n" + + "TRANSP:OPAQUE\n" + + "END:VEVENT\n" + + "END:VCALENDAR"; + Calendar calendar = new Calendar(); + calendar.setCalendarData(calendarString.getBytes()); + DateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ssX"); + + Date date = format.parse("2016-12-13 22:59:59+05"); + Assert.assertTrue(!calendar.checkMoment(date)); + date = format.parse("2016-12-13 23:00:01+05"); + Assert.assertTrue(calendar.checkMoment(date)); + + date = format.parse("2016-12-13 06:59:59+05"); + Assert.assertTrue(calendar.checkMoment(date)); + date = format.parse("2016-12-13 07:00:01+05"); + Assert.assertTrue(!calendar.checkMoment(date)); + } +} diff --git a/test/org/traccar/protocol/AutoGradeProtocolDecoderTest.java b/test/org/traccar/protocol/AutoGradeProtocolDecoderTest.java index 58e2e4065..a1b715f1c 100644 --- a/test/org/traccar/protocol/AutoGradeProtocolDecoderTest.java +++ b/test/org/traccar/protocol/AutoGradeProtocolDecoderTest.java @@ -11,6 +11,9 @@ public class AutoGradeProtocolDecoderTest extends ProtocolTest { AutoGradeProtocolDecoder decoder = new AutoGradeProtocolDecoder(new AutoGradeProtocol()); verifyPosition(decoder, text( + "(000000001637868324027912356171116A2250.7611N07556.9425E000.9024427197.36\u008eA0000B0000C0000D0000E0000K0000L0000M0000N0000O0000)")); + + verifyPosition(decoder, text( "(000000007322865733022629240170415A1001.1971N07618.1375E0.000145312128.59?A0024B0024C0000D0000E0000K0000L0000M0000N0000O0000")); } diff --git a/test/org/traccar/protocol/H02ProtocolDecoderTest.java b/test/org/traccar/protocol/H02ProtocolDecoderTest.java index d1b2d3198..62c4320e8 100644 --- a/test/org/traccar/protocol/H02ProtocolDecoderTest.java +++ b/test/org/traccar/protocol/H02ProtocolDecoderTest.java @@ -10,6 +10,9 @@ public class H02ProtocolDecoderTest extends ProtocolTest { H02ProtocolDecoder decoder = new H02ProtocolDecoder(new H02Protocol()); + verifyAttributes(decoder, buffer( + "*HQ,1600068812,NBR,141335,262,02,255,6,431,17003,26,431,11101,13,431,6353,13,431,22172,13,431,11093,13,431,60861,10,151216,FFFFFBFF#")); + verifyPosition(decoder, buffer( "*HQ,353588020068342,V1,084436,A,3257.01525,N,00655.03865,W,57.78,40,011216,FFFBFFFF,25c,a, 154,b04c#")); diff --git a/test/org/traccar/protocol/UproProtocolDecoderTest.java b/test/org/traccar/protocol/UproProtocolDecoderTest.java index 3af62da08..270caeab5 100644 --- a/test/org/traccar/protocol/UproProtocolDecoderTest.java +++ b/test/org/traccar/protocol/UproProtocolDecoderTest.java @@ -11,6 +11,9 @@ public class UproProtocolDecoderTest extends ProtocolTest { UproProtocolDecoder decoder = new UproProtocolDecoder(new UproProtocol()); verifyPosition(decoder, binary( + "2a4d473230313836383530303032303030343836372c414226413035303032343138313438373536303636303131373732323030303031313132313626583331302c3236302c34383837322c353639312c37333b34383837322c3732322c38363b34383837322c353639332c38383b34383837322c323336332c39303b34383837322c323336322c393726423030303030303030303026573030264e3230265a31342659313430303323")); + + verifyPosition(decoder, binary( "2a4d473230303639333530323030303033353537332c42412641303834313237333332363334353230373033383933373630303030303235313131362642303130303030303030302647303036323030264d393930264e3235264f3035303026433030313a363b363926510411058c0c125c0d0a2fff4237ee614d66454c140826555f50000000000300000000000000000026543139333723")); verifyPosition(decoder, buffer( diff --git a/test/org/traccar/protocol/WialonProtocolDecoderTest.java b/test/org/traccar/protocol/WialonProtocolDecoderTest.java index dd5ffd3a1..79eb2d009 100644 --- a/test/org/traccar/protocol/WialonProtocolDecoderTest.java +++ b/test/org/traccar/protocol/WialonProtocolDecoderTest.java @@ -20,6 +20,12 @@ public class WialonProtocolDecoderTest extends ProtocolTest { "#P#")); verifyPosition(decoder, text( + "#D#151216;135910;5321.1466;N;04441.7929;E;87;156;265.000000;12;1.000000;241;NA;NA;NA;odo:2:0.000000,total_fuel:1:430087,can_fls:1:201,can_taho:1:11623,can_mileage:1:140367515")); + + verifyPosition(decoder, text( + "#D#151216;140203;5312.59514;N;04830.37834;E;53;273;NA;10;NA;NA;NA;NA;NA;EvId:1:1,Board:2:12.81,Accum:2:4.28")); + + verifyPosition(decoder, text( "#SD#270413;205601;5544.6025;N;03739.6834;E;1;2;3;4"), position("2013-04-27 20:56:01.000", true, 55.74338, 37.66139)); diff --git a/tools/swagger2html.py b/tools/swagger2html.py new file mode 100755 index 000000000..a3488835c --- /dev/null +++ b/tools/swagger2html.py @@ -0,0 +1,354 @@ +#!/usr/bin/python + +import sys, argparse, json, re + +def handleException(etype, e=None): + if etype == 'KeyError': + print "Error: Required property {} not found".format(e) + elif etype == 'IOError': + print "Error ({}): {}".format(e.errno, e.strerror) + elif etype == 'ValueError': + print "Error: Unable to parse input as JSON" + elif etype == 'Custom': + print e + sys.exit(1) + +def get_json(filename): + try: + with open(filename) as json_file: + json_data = json.load(json_file) + return json_data + except IOError as e: + handleException('IOError', e) + except ValueError: + handleException('ValueError') + except: + print "Unexpected error: {}".format(sys.exc_info()[0]) + raise + +def write_file(filename, body): + try: + with open(filename, 'w') as md_file: + md_file.write(body) + except IOError as e: + handleException('IOError', e) + +def make_header(json_data): + try: + if not 'swagger' in json_data: + raise KeyError + info = json_data['info'] + md = "<h1>{}</h1>\n".format(info['title']) + md += "<p>Version: {}</p>\n".format(info['version']) + if 'license' in info: + md += "<p>License: " + license = info['license'] + if 'url' in license: + md += '<a href="{}">{}</a>'.format(license['url'], license['name']) + else: + md += license['name'] + md += '</p>\n' + if 'contact' in info: + contact = info['contact'] + if 'name' in contact or 'email' in contact: + md += '<p>Contact: ' + if not 'name' in contact: + md += '<a href="mailto:{0}">{0}</a>'.format(contact['email']) + elif not 'email' in contact: + md += contact['name'] + else: + md += '{0} <<a href="mailto:{1}">{1}</a>'.format(contact['name'], contact['email']) + md += ' \n' + if 'url' in contact: + md += "<p>Website: {}</p>\n".format(contact['url']) + if 'termsOfService' in info: + md += '<p>Terms of Service: {}</p>\n'.format(info['termsOfService']) + if 'host' in json_data: + md += '<p>Base URL: ' + base = json_data['host'] + if 'basePath' in json_data: + base += json_data['basePath'] + else: + base += '/' + if 'schemes' in json_data: + md += (', ').join(map( + lambda x: '<a href="{0}://{1}">{0}://{1}</a>'.format(x, base), + json_data['schemes'] + )) + else: + md += '<a href="{0}">{0}</a>'.format(base) + md += '</p>\n' + if 'description' in info: + md += '<p>Description: {}</p>\n'.format(info['description']) + md += '\n' + return md + except KeyErrori as e: + handleException('KeyError', e) + +def make_ref(ref): + href = ref.split('/')[1:] + return '<a href="#{}">{}</a>'.format('_'.join(href), href[-1]) + +def get_ref(ref, raw): + keys = ref.split('/') + return raw[keys[1]][keys[2]] + +def make_html(s): + reg = re.compile(r"[*_]{3}(.+?)[*_]{3}") + s = reg.sub(r"<strong><em>\1</em></strong>", s) + reg = re.compile(r"[*_]{2}(.+?)[*_]{2}") + s = reg.sub(r"<strong>\1</strong>", s) + reg = re.compile(r"[*_](.+?)[*_]") + s = reg.sub(r"<em>\1</em>", s) + reg = re.compile(r"\`(.+?)\`") + s = reg.sub(r"<code>\1</code>", s) + return s + +def make_table(data): + md = '<table class="table-bordered">\n' + md += ' <thead>\n' + md += ' <tr>\n' + for col in data[0]: + md += ' <td>{}</td>\n'.format(col) + md += ' </tr>\n' + md += ' </thead>\n' + md += ' <tbody>\n' + for row in data[1:]: + md += ' <tr>\n' + for cell in row: + md += ' <td>{}</td>\n'.format(cell) + md += ' </tr>\n' + md += ' </tbody>\n' + md += '</table>\n' + return md + +def make_params_table(itemsraw, raw): + items = [] + for item in itemsraw: + if '$ref' in item: + items.append(get_ref(item['$ref'], raw)) + else: + items.append(item) + try: + fields = list(set([]).union(*map(lambda x: x.keys(), items))) + row = [ 'Name', 'ParamType' ] + if 'description' in fields: + row.append('Description') + if 'required' in fields: + row.append('Required') + if 'type' in fields: + row.append('DataType') + if 'schema' in fields: + row.append('Schema') + table = [ row ] + for item in items: + row = [ "<em>{}</em>".format(item['name']), item['in'] ] + if 'description' in fields: + if 'description' in item: + row.append(make_html(item['description'])) + else: + row.append('') + if 'required' in fields: + required = 'False' + if 'required' in item and item['required']: + required = "<strong>True</strong>" + row.append(required) + if 'type' in fields: + type = '' + if 'type' in item: + if item['type'] == 'array': + type = "[ <em>{}</em> ]".format(item['items']['type']) + else: + type = item['type'] + if 'format' in item: + type += " ({})".format(item['format']) + type = "<em>{}</em>".format(type) + row.append(type) + if 'schema' in fields: + if 'schema' in item: + if '$ref' in item['schema']: + row.append(make_ref(item['schema']['$ref'])) + else: + row.append('') + table.append(row) + return make_table(table) + except KeyError as e: + handleException('KeyError', e) + +def make_responses_table(responses): + try: + fields = list( + set([]).union(*map(lambda x: x.keys(), + map(lambda x: responses[x], responses.keys()) + )) + ) + row = [ 'Status Code', 'Description' ] + if 'headers' in fields: + row.append('Headers') + if 'schema' in fields: + row.append('Schema') + if 'examples' in fields: + row.append('Examples') + table = [ row ] + for key in sorted(responses): + response = responses[key] + row = [ "<em>{}</em>".format(key), make_html(response['description']) ] + if 'headers' in fields: + header = '' + if 'headers' in response: + hrow = [] + for header, h_obj in response['headers'].iteritems(): + hrow += "{} ({})".format(header, h_obj['type']) + if 'description' in h_obj: + hrow += ": {}".format(h_obj['description']) + header = ' \n'.join(hrow) + row.append(header) + if 'schema' in fields: + schema = '' + if 'schema' in response: + if '$ref' in response['schema']: + schema += make_ref(response['schema']['$ref']) + if 'type' in response['schema']: + if response['schema']['type'] == 'array': + if '$ref' in response['schema']['items']: + schema += make_ref(response['schema']['items']['$ref']) + schema = "[ {} ]".format(schema) + row.append(schema) + if 'examples' in fields: + if 'examples' in response: + row.append(response['examples']) + else: + row.append('') + table.append(row) + return make_table(table) + except KeyError as e: + handleException('KeyError', e) + +def make_paths(sections, json_data): + md = '<h2><a name="paths"></a>Paths</h2>\n' + for key in sorted(sections): + md += '<h3><a name="paths_{0}"></a>{0}</h3>\n'.format(key) + for section in sections[key]: + md += '<h4><a name="{}"></a><code>{}</code></h4>\n'.format( + section['href'], section['title'] + ) + operation = section['operation'] + if 'summary' in operation: + md += '<p>Summary: {}</p>\n'.format(make_html(operation['summary'])) + if 'description' in operation: + md += '<p>Description: {}</p>\n'.format(make_html(operation['description'])) + md += '<h5>Parameters</h5>\n' + if 'parameters' in operation and len(operation['parameters']) > 0: + md += make_params_table(operation['parameters'], json_data) + else: + md += "<p><em>None</em></p>\n" + md += '<h5>Responses</h5>\n' + md += make_responses_table(operation['responses']) + md += '\n' + md += '\n' + return md + +def make_contents(path_section, json_data): + md = '<h3>Contents</h3>\n' + md += '<ul>\n' + md += ' <li><a href="#paths">Paths</a>\n' + md += ' <ul>\n' + for key in sorted(path_section): + md += ' <li><a href="#paths_{0}">{0}</a>\n'.format(key) + md += ' <ul>\n' + for section in path_section[key]: + md += ' <li><a href="#{}">{}</a></li>\n'.format( + section['href'], section['title'] + ) + md += ' </ul>\n' + md += ' </li>\n' + md += ' </ul>\n' + md += ' </li>\n' + md += ' <li><a href="#definitions">Models</a>\n' + md += ' <ul>\n' + for key in sorted(json_data['definitions']): + md += ' <li><a href="#definitions_{0}">{0}</a></li>\n'.format(key) + md += ' </ul>\n' + md += ' </li>\n' + md += '</ul>\n' + return md + +def make_definitions(json_data): + md = '<h2><a name="definitions"></a>Models</h2>\n' + for name in sorted(json_data['definitions']): + md += '<h3><a name="definitions_{0}"></a>{0}</h3>\n'.format(name) + model = json_data['definitions'][name] + if 'properties' in model: + fields = list( + set(['type']).union( + *map(lambda x: x.keys(), + map(lambda x: model['properties'][x], model['properties'].keys()) + ) + ) + ) + row = [ 'Property', 'Type' ] + if 'description' in fields: + row.append('Description') + table = [ row ] + for key in sorted(model['properties']): + property = model['properties'][key] + row = [ "<em>{}</em>".format(key) ] + if 'type' in property: + type = property['type'] + if 'format' in property: + type += " ({})".format(property['format']) + row.append("<em>{}</em>".format(type)) + elif '$ref' in property: + row.append(make_ref(property['$ref'])) + else: + row.append('') + if 'description' in fields: + if 'description' in property: + row.append(make_html(property['description'])) + else: + row.append('') + table.append(row) + md += make_table(table) + return md + +def make_markdown(json_data): + path_sections = {} + for endpoint in json_data['paths']: + path_split = endpoint.split('/') + path_key = path_split[1] + if not path_key in path_sections: + path_sections[path_key] = [] + for method, operation in json_data['paths'][endpoint].iteritems(): + if 'operationId' in operation: + link = operation['operationId'] + else: + link = ''.join([ + c for c in endpoint if c not in ['/', '{', '}'] + ]) + path_sections[path_key].append({ + 'title': '{} {}'.format(method.upper(), endpoint), + 'href': 'paths_{}_{}'.format(link, method.upper()), + 'operation': operation + }) + md = make_header(json_data) + md += make_contents(path_sections, json_data) + md += make_paths(path_sections, json_data) + md += make_definitions(json_data) + return md + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('SPECFILE', help="path to swagger.json file") + parser.add_argument('OUTFILE', help="path to output HTML file") + args = parser.parse_args() + + marked_down = make_markdown(get_json(args.SPECFILE)) + + if args.OUTFILE: + write_file(args.OUTFILE, marked_down) + print " success: {}".format(args.OUTFILE) + else: + print marked_down + +if __name__ == '__main__': + main() |