aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pom.xml5
-rw-r--r--schema/changelog-3.10.xml45
-rw-r--r--schema/changelog-master.xml1
-rw-r--r--setup/default.xml38
-rwxr-xr-xsetup/package.sh2
-rw-r--r--src/org/traccar/BaseProtocolDecoder.java27
-rw-r--r--src/org/traccar/Context.java14
-rw-r--r--src/org/traccar/ExtendedObjectDecoder.java5
-rw-r--r--src/org/traccar/api/resource/CalendarPermissionResource.java57
-rw-r--r--src/org/traccar/api/resource/CalendarResource.java85
-rw-r--r--src/org/traccar/database/CalendarManager.java112
-rw-r--r--src/org/traccar/database/DataManager.java43
-rw-r--r--src/org/traccar/database/PermissionsManager.java6
-rw-r--r--src/org/traccar/database/QueryBuilder.java30
-rw-r--r--src/org/traccar/events/GeofenceEventHandler.java20
-rw-r--r--src/org/traccar/location/MozillaLocationProvider.java8
-rw-r--r--src/org/traccar/model/Calendar.java82
-rw-r--r--src/org/traccar/model/CalendarPermission.java40
-rw-r--r--src/org/traccar/model/Geofence.java9
-rw-r--r--src/org/traccar/protocol/AplicomProtocolDecoder.java5
-rw-r--r--src/org/traccar/protocol/AutoGradeProtocolDecoder.java2
-rw-r--r--src/org/traccar/web/WebServer.java2
-rw-r--r--swagger.json1042
-rw-r--r--templates/mail/alarm.vm2
-rw-r--r--templates/mail/deviceMoving.vm2
-rw-r--r--templates/mail/deviceOverspeed.vm2
-rw-r--r--templates/mail/deviceStopped.vm2
-rw-r--r--templates/mail/geofenceEnter.vm2
-rw-r--r--templates/mail/geofenceExit.vm2
-rw-r--r--templates/mail/ignitionOff.vm2
-rw-r--r--templates/mail/ignitionOn.vm2
-rw-r--r--templates/mail/maintenance.vm2
-rw-r--r--test/org/traccar/calendar/CalendarTest.java58
-rw-r--r--test/org/traccar/protocol/AutoGradeProtocolDecoderTest.java3
-rw-r--r--test/org/traccar/protocol/H02ProtocolDecoderTest.java3
-rw-r--r--test/org/traccar/protocol/UproProtocolDecoderTest.java3
-rw-r--r--test/org/traccar/protocol/WialonProtocolDecoderTest.java6
-rwxr-xr-xtools/swagger2html.py354
38 files changed, 1419 insertions, 706 deletions
diff --git a/pom.xml b/pom.xml
index 45df20efc..3d142b7cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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&deg;, $position.longitude&deg;#{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} &lt;<a href="mailto:{1}"&gt;{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()