aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/traccar/database
diff options
context:
space:
mode:
authorAnton Tananaev <anton.tananaev@gmail.com>2019-03-31 22:35:39 -0700
committerAnton Tananaev <anton.tananaev@gmail.com>2019-03-31 22:35:39 -0700
commit59416923dcb3a756eaf532cc4259f2f6625c0762 (patch)
tree9082dae6616deac8fda432b7bfd80e4a52b6d9dc /src/main/java/org/traccar/database
parent79a129dd6327d932133d6b9a50190d3f4927bff9 (diff)
downloadtrackermap-server-59416923dcb3a756eaf532cc4259f2f6625c0762.tar.gz
trackermap-server-59416923dcb3a756eaf532cc4259f2f6625c0762.tar.bz2
trackermap-server-59416923dcb3a756eaf532cc4259f2f6625c0762.zip
Convert project to gradle
Diffstat (limited to 'src/main/java/org/traccar/database')
-rw-r--r--src/main/java/org/traccar/database/ActiveDevice.java55
-rw-r--r--src/main/java/org/traccar/database/AttributesManager.java36
-rw-r--r--src/main/java/org/traccar/database/BaseObjectManager.java127
-rw-r--r--src/main/java/org/traccar/database/CalendarManager.java27
-rw-r--r--src/main/java/org/traccar/database/CommandsManager.java157
-rw-r--r--src/main/java/org/traccar/database/ConnectionManager.java218
-rw-r--r--src/main/java/org/traccar/database/DataManager.java478
-rw-r--r--src/main/java/org/traccar/database/DeviceManager.java419
-rw-r--r--src/main/java/org/traccar/database/DriversManager.java73
-rw-r--r--src/main/java/org/traccar/database/ExtendedObjectManager.java115
-rw-r--r--src/main/java/org/traccar/database/GeofenceManager.java66
-rw-r--r--src/main/java/org/traccar/database/GroupTree.java151
-rw-r--r--src/main/java/org/traccar/database/GroupsManager.java106
-rw-r--r--src/main/java/org/traccar/database/IdentityManager.java43
-rw-r--r--src/main/java/org/traccar/database/LdapProvider.java179
-rw-r--r--src/main/java/org/traccar/database/MailManager.java147
-rw-r--r--src/main/java/org/traccar/database/MaintenancesManager.java27
-rw-r--r--src/main/java/org/traccar/database/ManagableObjects.java27
-rw-r--r--src/main/java/org/traccar/database/MediaManager.java72
-rw-r--r--src/main/java/org/traccar/database/NotificationManager.java135
-rw-r--r--src/main/java/org/traccar/database/PermissionsManager.java459
-rw-r--r--src/main/java/org/traccar/database/QueryBuilder.java519
-rw-r--r--src/main/java/org/traccar/database/QueryExtended.java27
-rw-r--r--src/main/java/org/traccar/database/QueryIgnore.java26
-rw-r--r--src/main/java/org/traccar/database/SimpleObjectManager.java94
-rw-r--r--src/main/java/org/traccar/database/StatisticsManager.java160
-rw-r--r--src/main/java/org/traccar/database/UsersManager.java86
27 files changed, 4029 insertions, 0 deletions
diff --git a/src/main/java/org/traccar/database/ActiveDevice.java b/src/main/java/org/traccar/database/ActiveDevice.java
new file mode 100644
index 000000000..207fc454b
--- /dev/null
+++ b/src/main/java/org/traccar/database/ActiveDevice.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import io.netty.channel.Channel;
+import org.traccar.NetworkMessage;
+import org.traccar.Protocol;
+import org.traccar.model.Command;
+
+import java.net.SocketAddress;
+
+public class ActiveDevice {
+
+ private final long deviceId;
+ private final Protocol protocol;
+ private final Channel channel;
+ private final SocketAddress remoteAddress;
+
+ public ActiveDevice(long deviceId, Protocol protocol, Channel channel, SocketAddress remoteAddress) {
+ this.deviceId = deviceId;
+ this.protocol = protocol;
+ this.channel = channel;
+ this.remoteAddress = remoteAddress;
+ }
+
+ public Channel getChannel() {
+ return channel;
+ }
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void sendCommand(Command command) {
+ protocol.sendDataCommand(this, command);
+ }
+
+ public void write(Object message) {
+ channel.writeAndFlush(new NetworkMessage(message, remoteAddress));
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/AttributesManager.java b/src/main/java/org/traccar/database/AttributesManager.java
new file mode 100644
index 000000000..28816645a
--- /dev/null
+++ b/src/main/java/org/traccar/database/AttributesManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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 org.traccar.model.Attribute;
+
+public class AttributesManager extends ExtendedObjectManager<Attribute> {
+
+ public AttributesManager(DataManager dataManager) {
+ super(dataManager, Attribute.class);
+ }
+
+ @Override
+ public void updateCachedItem(Attribute attribute) {
+ Attribute cachedAttribute = getById(attribute.getId());
+ cachedAttribute.setDescription(attribute.getDescription());
+ cachedAttribute.setAttribute(attribute.getAttribute());
+ cachedAttribute.setExpression(attribute.getExpression());
+ cachedAttribute.setType(attribute.getType());
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/BaseObjectManager.java b/src/main/java/org/traccar/database/BaseObjectManager.java
new file mode 100644
index 000000000..8bf9ef860
--- /dev/null
+++ b/src/main/java/org/traccar/database/BaseObjectManager.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.model.BaseModel;
+
+public class BaseObjectManager<T extends BaseModel> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BaseObjectManager.class);
+
+ private final DataManager dataManager;
+
+ private Map<Long, T> items;
+ private Class<T> baseClass;
+
+ protected BaseObjectManager(DataManager dataManager, Class<T> baseClass) {
+ this.dataManager = dataManager;
+ this.baseClass = baseClass;
+ refreshItems();
+ }
+
+ protected final DataManager getDataManager() {
+ return dataManager;
+ }
+
+ protected final Class<T> getBaseClass() {
+ return baseClass;
+ }
+
+ public T getById(long itemId) {
+ return items.get(itemId);
+ }
+
+ public void refreshItems() {
+ if (dataManager != null) {
+ try {
+ Collection<T> databaseItems = dataManager.getObjects(baseClass);
+ if (items == null) {
+ items = new ConcurrentHashMap<>(databaseItems.size());
+ }
+ Set<Long> databaseItemIds = new HashSet<>();
+ for (T item : databaseItems) {
+ databaseItemIds.add(item.getId());
+ if (items.containsKey(item.getId())) {
+ updateCachedItem(item);
+ } else {
+ addNewItem(item);
+ }
+ }
+ for (Long cachedItemId : items.keySet()) {
+ if (!databaseItemIds.contains(cachedItemId)) {
+ removeCachedItem(cachedItemId);
+ }
+ }
+ } catch (SQLException error) {
+ LOGGER.warn("Error refreshing items", error);
+ }
+ }
+ }
+
+ protected void addNewItem(T item) {
+ items.put(item.getId(), item);
+ }
+
+ public void addItem(T item) throws SQLException {
+ dataManager.addObject(item);
+ addNewItem(item);
+ }
+
+ protected void updateCachedItem(T item) {
+ items.put(item.getId(), item);
+ }
+
+ public void updateItem(T item) throws SQLException {
+ dataManager.updateObject(item);
+ updateCachedItem(item);
+ }
+
+ protected void removeCachedItem(long itemId) {
+ items.remove(itemId);
+ }
+
+ public void removeItem(long itemId) throws SQLException {
+ BaseModel item = getById(itemId);
+ if (item != null) {
+ dataManager.removeObject(baseClass, itemId);
+ removeCachedItem(itemId);
+ }
+ }
+
+ public final Collection<T> getItems(Set<Long> itemIds) {
+ Collection<T> result = new LinkedList<>();
+ for (long itemId : itemIds) {
+ result.add(getById(itemId));
+ }
+ return result;
+ }
+
+ public Set<Long> getAllItems() {
+ return items.keySet();
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/CalendarManager.java b/src/main/java/org/traccar/database/CalendarManager.java
new file mode 100644
index 000000000..44ced1082
--- /dev/null
+++ b/src/main/java/org/traccar/database/CalendarManager.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 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 org.traccar.model.Calendar;
+
+public class CalendarManager extends SimpleObjectManager<Calendar> {
+
+ public CalendarManager(DataManager dataManager) {
+ super(dataManager, Calendar.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/CommandsManager.java b/src/main/java/org/traccar/database/CommandsManager.java
new file mode 100644
index 000000000..d6fdd66ca
--- /dev/null
+++ b/src/main/java/org/traccar/database/CommandsManager.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.BaseProtocol;
+import org.traccar.Context;
+import org.traccar.model.Command;
+import org.traccar.model.Typed;
+import org.traccar.model.Position;
+
+public class CommandsManager extends ExtendedObjectManager<Command> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CommandsManager.class);
+
+ private final Map<Long, Queue<Command>> deviceQueues = new ConcurrentHashMap<>();
+
+ private boolean queueing;
+
+ public CommandsManager(DataManager dataManager, boolean queueing) {
+ super(dataManager, Command.class);
+ this.queueing = queueing;
+ }
+
+ public boolean checkDeviceCommand(long deviceId, long commandId) {
+ return !getAllDeviceItems(deviceId).contains(commandId);
+ }
+
+ public boolean sendCommand(Command command) throws Exception {
+ long deviceId = command.getDeviceId();
+ if (command.getId() != 0) {
+ command = getById(command.getId()).clone();
+ command.setDeviceId(deviceId);
+ }
+ if (command.getTextChannel()) {
+ Position lastPosition = Context.getIdentityManager().getLastPosition(deviceId);
+ String phone = Context.getIdentityManager().getById(deviceId).getPhone();
+ if (lastPosition != null) {
+ BaseProtocol protocol = Context.getServerManager().getProtocol(lastPosition.getProtocol());
+ protocol.sendTextCommand(phone, command);
+ } else if (command.getType().equals(Command.TYPE_CUSTOM)) {
+ if (Context.getSmsManager() != null) {
+ Context.getSmsManager().sendMessageSync(phone, command.getString(Command.KEY_DATA), true);
+ } else {
+ throw new RuntimeException("SMS is not enabled");
+ }
+ } else {
+ throw new RuntimeException("Command " + command.getType() + " is not supported");
+ }
+ } else {
+ ActiveDevice activeDevice = Context.getConnectionManager().getActiveDevice(deviceId);
+ if (activeDevice != null) {
+ activeDevice.sendCommand(command);
+ } else if (!queueing) {
+ throw new RuntimeException("Device is not online");
+ } else {
+ getDeviceQueue(deviceId).add(command);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public Collection<Long> getSupportedCommands(long deviceId) {
+ List<Long> result = new ArrayList<>();
+ Position lastPosition = Context.getIdentityManager().getLastPosition(deviceId);
+ for (long commandId : getAllDeviceItems(deviceId)) {
+ Command command = getById(commandId);
+ if (lastPosition != null) {
+ BaseProtocol protocol = Context.getServerManager().getProtocol(lastPosition.getProtocol());
+ if (command.getTextChannel() && protocol.getSupportedTextCommands().contains(command.getType())
+ || !command.getTextChannel()
+ && protocol.getSupportedDataCommands().contains(command.getType())) {
+ result.add(commandId);
+ }
+ } else if (command.getType().equals(Command.TYPE_CUSTOM)) {
+ result.add(commandId);
+ }
+ }
+ return result;
+ }
+
+ public Collection<Typed> getCommandTypes(long deviceId, boolean textChannel) {
+ List<Typed> result = new ArrayList<>();
+ Position lastPosition = Context.getIdentityManager().getLastPosition(deviceId);
+ if (lastPosition != null) {
+ BaseProtocol protocol = Context.getServerManager().getProtocol(lastPosition.getProtocol());
+ Collection<String> commands;
+ commands = textChannel ? protocol.getSupportedTextCommands() : protocol.getSupportedDataCommands();
+ for (String commandKey : commands) {
+ result.add(new Typed(commandKey));
+ }
+ } else {
+ result.add(new Typed(Command.TYPE_CUSTOM));
+ }
+ return result;
+ }
+
+ public Collection<Typed> getAllCommandTypes() {
+ List<Typed> result = new ArrayList<>();
+ Field[] fields = Command.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("TYPE_")) {
+ try {
+ result.add(new Typed(field.get(null).toString()));
+ } catch (IllegalArgumentException | IllegalAccessException error) {
+ LOGGER.warn("Get command types error", error);
+ }
+ }
+ }
+ return result;
+ }
+
+ private Queue<Command> getDeviceQueue(long deviceId) {
+ if (!deviceQueues.containsKey(deviceId)) {
+ deviceQueues.put(deviceId, new ConcurrentLinkedQueue<Command>());
+ }
+ return deviceQueues.get(deviceId);
+ }
+
+ public void sendQueuedCommands(ActiveDevice activeDevice) {
+ Queue<Command> deviceQueue = deviceQueues.get(activeDevice.getDeviceId());
+ if (deviceQueue != null) {
+ Command command = deviceQueue.poll();
+ while (command != null) {
+ activeDevice.sendCommand(command);
+ command = deviceQueue.poll();
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/ConnectionManager.java b/src/main/java/org/traccar/database/ConnectionManager.java
new file mode 100644
index 000000000..8bae1ea93
--- /dev/null
+++ b/src/main/java/org/traccar/database/ConnectionManager.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import io.netty.channel.Channel;
+import io.netty.util.Timeout;
+import io.netty.util.TimerTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.GlobalTimer;
+import org.traccar.Main;
+import org.traccar.Protocol;
+import org.traccar.handler.events.MotionEventHandler;
+import org.traccar.handler.events.OverspeedEventHandler;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceState;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+import java.net.SocketAddress;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+public class ConnectionManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
+
+ private static final long DEFAULT_TIMEOUT = 600;
+
+ private final long deviceTimeout;
+ private final boolean enableStatusEvents;
+ private final boolean updateDeviceState;
+
+ private final Map<Long, ActiveDevice> activeDevices = new ConcurrentHashMap<>();
+ private final Map<Long, Set<UpdateListener>> listeners = new ConcurrentHashMap<>();
+ private final Map<Long, Timeout> timeouts = new ConcurrentHashMap<>();
+
+ public ConnectionManager() {
+ deviceTimeout = Context.getConfig().getLong("status.timeout", DEFAULT_TIMEOUT) * 1000;
+ enableStatusEvents = Context.getConfig().getBoolean("event.enable");
+ updateDeviceState = Context.getConfig().getBoolean("status.updateDeviceState");
+ }
+
+ public void addActiveDevice(long deviceId, Protocol protocol, Channel channel, SocketAddress remoteAddress) {
+ activeDevices.put(deviceId, new ActiveDevice(deviceId, protocol, channel, remoteAddress));
+ }
+
+ public void removeActiveDevice(Channel channel) {
+ for (ActiveDevice activeDevice : activeDevices.values()) {
+ if (activeDevice.getChannel() == channel) {
+ updateDevice(activeDevice.getDeviceId(), Device.STATUS_OFFLINE, null);
+ activeDevices.remove(activeDevice.getDeviceId());
+ break;
+ }
+ }
+ }
+
+ public ActiveDevice getActiveDevice(long deviceId) {
+ return activeDevices.get(deviceId);
+ }
+
+ public void updateDevice(final long deviceId, String status, Date time) {
+ Device device = Context.getIdentityManager().getById(deviceId);
+ if (device == null) {
+ return;
+ }
+
+ String oldStatus = device.getStatus();
+ device.setStatus(status);
+
+ if (enableStatusEvents && !status.equals(oldStatus)) {
+ String eventType;
+ Map<Event, Position> events = new HashMap<>();
+ switch (status) {
+ case Device.STATUS_ONLINE:
+ eventType = Event.TYPE_DEVICE_ONLINE;
+ break;
+ case Device.STATUS_UNKNOWN:
+ eventType = Event.TYPE_DEVICE_UNKNOWN;
+ if (updateDeviceState) {
+ events.putAll(updateDeviceState(deviceId));
+ }
+ break;
+ default:
+ eventType = Event.TYPE_DEVICE_OFFLINE;
+ if (updateDeviceState) {
+ events.putAll(updateDeviceState(deviceId));
+ }
+ break;
+ }
+ events.put(new Event(eventType, deviceId), null);
+ Context.getNotificationManager().updateEvents(events);
+ }
+
+ Timeout timeout = timeouts.remove(deviceId);
+ if (timeout != null) {
+ timeout.cancel();
+ }
+
+ if (time != null) {
+ device.setLastUpdate(time);
+ }
+
+ if (status.equals(Device.STATUS_ONLINE)) {
+ timeouts.put(deviceId, GlobalTimer.getTimer().newTimeout(new TimerTask() {
+ @Override
+ public void run(Timeout timeout) {
+ if (!timeout.isCancelled()) {
+ updateDevice(deviceId, Device.STATUS_UNKNOWN, null);
+ }
+ }
+ }, deviceTimeout, TimeUnit.MILLISECONDS));
+ }
+
+ try {
+ Context.getDeviceManager().updateDeviceStatus(device);
+ } catch (SQLException error) {
+ LOGGER.warn("Update device status error", error);
+ }
+
+ updateDevice(device);
+
+ if (status.equals(Device.STATUS_ONLINE) && !oldStatus.equals(Device.STATUS_ONLINE)) {
+ Context.getCommandsManager().sendQueuedCommands(getActiveDevice(deviceId));
+ }
+ }
+
+ public Map<Event, Position> updateDeviceState(long deviceId) {
+ DeviceState deviceState = Context.getDeviceManager().getDeviceState(deviceId);
+ Map<Event, Position> result = new HashMap<>();
+
+ Map<Event, Position> event = Main.getInjector()
+ .getInstance(MotionEventHandler.class).updateMotionState(deviceState);
+ if (event != null) {
+ result.putAll(event);
+ }
+
+ event = Main.getInjector().getInstance(OverspeedEventHandler.class)
+ .updateOverspeedState(deviceState, Context.getDeviceManager().
+ lookupAttributeDouble(deviceId, OverspeedEventHandler.ATTRIBUTE_SPEED_LIMIT, 0, false));
+ if (event != null) {
+ result.putAll(event);
+ }
+
+ return result;
+ }
+
+ public synchronized void updateDevice(Device device) {
+ for (long userId : Context.getPermissionsManager().getDeviceUsers(device.getId())) {
+ if (listeners.containsKey(userId)) {
+ for (UpdateListener listener : listeners.get(userId)) {
+ listener.onUpdateDevice(device);
+ }
+ }
+ }
+ }
+
+ public synchronized void updatePosition(Position position) {
+ long deviceId = position.getDeviceId();
+
+ for (long userId : Context.getPermissionsManager().getDeviceUsers(deviceId)) {
+ if (listeners.containsKey(userId)) {
+ for (UpdateListener listener : listeners.get(userId)) {
+ listener.onUpdatePosition(position);
+ }
+ }
+ }
+ }
+
+ public synchronized void updateEvent(long userId, Event event) {
+ if (listeners.containsKey(userId)) {
+ for (UpdateListener listener : listeners.get(userId)) {
+ listener.onUpdateEvent(event);
+ }
+ }
+ }
+
+ public interface UpdateListener {
+ void onUpdateDevice(Device device);
+ void onUpdatePosition(Position position);
+ void onUpdateEvent(Event event);
+ }
+
+ public synchronized void addListener(long userId, UpdateListener listener) {
+ if (!listeners.containsKey(userId)) {
+ listeners.put(userId, new HashSet<UpdateListener>());
+ }
+ listeners.get(userId).add(listener);
+ }
+
+ public synchronized void removeListener(long userId, UpdateListener listener) {
+ if (!listeners.containsKey(userId)) {
+ listeners.put(userId, new HashSet<UpdateListener>());
+ }
+ listeners.get(userId).remove(listener);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/DataManager.java b/src/main/java/org/traccar/database/DataManager.java
new file mode 100644
index 000000000..8e9071736
--- /dev/null
+++ b/src/main/java/org/traccar/database/DataManager.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright 2012 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.beans.Introspector;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+import liquibase.Contexts;
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.exception.LiquibaseException;
+import liquibase.resource.FileSystemResourceAccessor;
+import liquibase.resource.ResourceAccessor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.Context;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Attribute;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.model.ManagedUser;
+import org.traccar.model.Notification;
+import org.traccar.model.Permission;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Calendar;
+import org.traccar.model.Command;
+import org.traccar.model.Position;
+import org.traccar.model.Server;
+import org.traccar.model.Statistics;
+import org.traccar.model.User;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+
+public class DataManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DataManager.class);
+
+ public static final String ACTION_SELECT_ALL = "selectAll";
+ public static final String ACTION_SELECT = "select";
+ public static final String ACTION_INSERT = "insert";
+ public static final String ACTION_UPDATE = "update";
+ public static final String ACTION_DELETE = "delete";
+
+ private final Config config;
+
+ private DataSource dataSource;
+
+ private boolean generateQueries;
+
+ private boolean forceLdap;
+
+ public DataManager(Config config) throws Exception {
+ this.config = config;
+
+ forceLdap = config.getBoolean("ldap.force");
+
+ initDatabase();
+ initDatabaseSchema();
+ }
+
+ private void initDatabase() throws Exception {
+
+ String jndiName = config.getString("database.jndi");
+
+ if (jndiName != null) {
+
+ dataSource = (DataSource) new InitialContext().lookup(jndiName);
+
+ } else {
+
+ String driverFile = config.getString("database.driverFile");
+ if (driverFile != null) {
+ ClassLoader classLoader = ClassLoader.getSystemClassLoader();
+ try {
+ Method method = classLoader.getClass().getDeclaredMethod("addURL", URL.class);
+ method.setAccessible(true);
+ method.invoke(classLoader, new File(driverFile).toURI().toURL());
+ } catch (NoSuchMethodException e) {
+ Method method = classLoader.getClass()
+ .getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
+ method.setAccessible(true);
+ method.invoke(classLoader, driverFile);
+ }
+ }
+
+ String driver = config.getString("database.driver");
+ if (driver != null) {
+ Class.forName(driver);
+ }
+
+ HikariConfig hikariConfig = new HikariConfig();
+ hikariConfig.setDriverClassName(config.getString("database.driver"));
+ hikariConfig.setJdbcUrl(config.getString("database.url"));
+ hikariConfig.setUsername(config.getString("database.user"));
+ hikariConfig.setPassword(config.getString("database.password"));
+ hikariConfig.setConnectionInitSql(config.getString("database.checkConnection", "SELECT 1"));
+ hikariConfig.setIdleTimeout(600000);
+
+ int maxPoolSize = config.getInteger("database.maxPoolSize");
+
+ if (maxPoolSize != 0) {
+ hikariConfig.setMaximumPoolSize(maxPoolSize);
+ }
+
+ generateQueries = config.getBoolean("database.generateQueries");
+
+ dataSource = new HikariDataSource(hikariConfig);
+
+ }
+ }
+
+ public static String constructObjectQuery(String action, Class<?> clazz, boolean extended) {
+ switch (action) {
+ case ACTION_INSERT:
+ case ACTION_UPDATE:
+ StringBuilder result = new StringBuilder();
+ StringBuilder fields = new StringBuilder();
+ StringBuilder values = new StringBuilder();
+
+ Set<Method> methods = new HashSet<>(Arrays.asList(clazz.getMethods()));
+ methods.removeAll(Arrays.asList(Object.class.getMethods()));
+ methods.removeAll(Arrays.asList(BaseModel.class.getMethods()));
+ for (Method method : methods) {
+ boolean skip;
+ if (extended) {
+ skip = !method.isAnnotationPresent(QueryExtended.class);
+ } else {
+ skip = method.isAnnotationPresent(QueryIgnore.class)
+ || method.isAnnotationPresent(QueryExtended.class) && !action.equals(ACTION_INSERT);
+ }
+ if (!skip && method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
+ String name = Introspector.decapitalize(method.getName().substring(3));
+ if (action.equals(ACTION_INSERT)) {
+ fields.append(name).append(", ");
+ values.append(":").append(name).append(", ");
+ } else {
+ fields.append(name).append(" = :").append(name).append(", ");
+ }
+ }
+ }
+ fields.setLength(fields.length() - 2);
+ if (action.equals(ACTION_INSERT)) {
+ values.setLength(values.length() - 2);
+ result.append("INSERT INTO ").append(getObjectsTableName(clazz)).append(" (");
+ result.append(fields).append(") ");
+ result.append("VALUES (").append(values).append(")");
+ } else {
+ result.append("UPDATE ").append(getObjectsTableName(clazz)).append(" SET ");
+ result.append(fields);
+ result.append(" WHERE id = :id");
+ }
+ return result.toString();
+ case ACTION_SELECT_ALL:
+ return "SELECT * FROM " + getObjectsTableName(clazz);
+ case ACTION_SELECT:
+ return "SELECT * FROM " + getObjectsTableName(clazz) + " WHERE id = :id";
+ case ACTION_DELETE:
+ return "DELETE FROM " + getObjectsTableName(clazz) + " WHERE id = :id";
+ default:
+ throw new IllegalArgumentException("Unknown action");
+ }
+ }
+
+ public static String constructPermissionQuery(String action, Class<?> owner, Class<?> property) {
+ switch (action) {
+ case ACTION_SELECT_ALL:
+ return "SELECT " + makeNameId(owner) + ", " + makeNameId(property) + " FROM "
+ + getPermissionsTableName(owner, property);
+ case ACTION_INSERT:
+ return "INSERT INTO " + getPermissionsTableName(owner, property)
+ + " (" + makeNameId(owner) + ", " + makeNameId(property) + ") VALUES (:"
+ + makeNameId(owner) + ", :" + makeNameId(property) + ")";
+ case ACTION_DELETE:
+ return "DELETE FROM " + getPermissionsTableName(owner, property)
+ + " WHERE " + makeNameId(owner) + " = :" + makeNameId(owner)
+ + " AND " + makeNameId(property) + " = :" + makeNameId(property);
+ default:
+ throw new IllegalArgumentException("Unknown action");
+ }
+ }
+
+ private String getQuery(String key) {
+ String query = config.getString(key);
+ if (query == null) {
+ LOGGER.info("Query not provided: " + key);
+ }
+ return query;
+ }
+
+ public String getQuery(String action, Class<?> clazz) {
+ return getQuery(action, clazz, false);
+ }
+
+ public String getQuery(String action, Class<?> clazz, boolean extended) {
+ String queryName;
+ if (action.equals(ACTION_SELECT_ALL)) {
+ queryName = "database.select" + clazz.getSimpleName() + "s";
+ } else {
+ queryName = "database." + action.toLowerCase() + clazz.getSimpleName();
+ if (extended) {
+ queryName += "Extended";
+ }
+ }
+ String query = config.getString(queryName);
+ if (query == null) {
+ if (generateQueries) {
+ query = constructObjectQuery(action, clazz, extended);
+ config.setString(queryName, query);
+ } else {
+ LOGGER.info("Query not provided: " + queryName);
+ }
+ }
+
+ return query;
+ }
+
+ public String getQuery(String action, Class<?> owner, Class<?> property) {
+ String queryName;
+ switch (action) {
+ case ACTION_SELECT_ALL:
+ queryName = "database.select" + owner.getSimpleName() + property.getSimpleName() + "s";
+ break;
+ case ACTION_INSERT:
+ queryName = "database.link" + owner.getSimpleName() + property.getSimpleName();
+ break;
+ default:
+ queryName = "database.unlink" + owner.getSimpleName() + property.getSimpleName();
+ break;
+ }
+ String query = config.getString(queryName);
+ if (query == null) {
+ if (generateQueries) {
+ query = constructPermissionQuery(action, owner,
+ property.equals(User.class) ? ManagedUser.class : property);
+ config.setString(queryName, query);
+ } else {
+ LOGGER.info("Query not provided: " + queryName);
+ }
+ }
+
+ return query;
+ }
+
+ private static String getPermissionsTableName(Class<?> owner, Class<?> property) {
+ String propertyName = property.getSimpleName();
+ if (propertyName.equals("ManagedUser")) {
+ propertyName = "User";
+ }
+ return "tc_" + Introspector.decapitalize(owner.getSimpleName())
+ + "_" + Introspector.decapitalize(propertyName);
+ }
+
+ private static String getObjectsTableName(Class<?> clazz) {
+ String result = "tc_" + Introspector.decapitalize(clazz.getSimpleName());
+ // Add "s" ending if object name is not plural already
+ if (!result.endsWith("s")) {
+ result += "s";
+ }
+ return result;
+ }
+
+ private void initDatabaseSchema() throws SQLException, LiquibaseException {
+
+ if (config.hasKey("database.changelog")) {
+
+ ResourceAccessor resourceAccessor = new FileSystemResourceAccessor();
+
+ Database database = DatabaseFactory.getInstance().openDatabase(
+ config.getString("database.url"),
+ config.getString("database.user"),
+ config.getString("database.password"),
+ config.getString("database.driver"),
+ null, null, null, resourceAccessor);
+
+ Liquibase liquibase = new Liquibase(
+ config.getString("database.changelog"), resourceAccessor, database);
+
+ liquibase.clearCheckSums();
+
+ liquibase.update(new Contexts());
+ }
+ }
+
+ public User login(String email, String password) throws SQLException {
+ User user = QueryBuilder.create(dataSource, getQuery("database.loginUser"))
+ .setString("email", email.trim())
+ .executeQuerySingle(User.class);
+ LdapProvider ldapProvider = Context.getLdapProvider();
+ if (user != null) {
+ if (ldapProvider != null && user.getLogin() != null && ldapProvider.login(user.getLogin(), password)
+ || !forceLdap && user.isPasswordValid(password)) {
+ return user;
+ }
+ } else {
+ if (ldapProvider != null && ldapProvider.login(email, password)) {
+ user = ldapProvider.getUser(email);
+ Context.getUsersManager().addItem(user);
+ return user;
+ }
+ }
+ return null;
+ }
+
+ public void updateDeviceStatus(Device device) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(ACTION_UPDATE, Device.class, true))
+ .setObject(device)
+ .executeUpdate();
+ }
+
+ public Collection<Position> getPositions(long deviceId, Date from, Date to) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectPositions"))
+ .setLong("deviceId", deviceId)
+ .setDate("from", from)
+ .setDate("to", to)
+ .executeQuery(Position.class);
+ }
+
+ public void updateLatestPosition(Position position) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.updateLatestPosition"))
+ .setDate("now", new Date())
+ .setObject(position)
+ .executeUpdate();
+ }
+
+ public Collection<Position> getLatestPositions() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectLatestPositions"))
+ .executeQuery(Position.class);
+ }
+
+ public void clearHistory() throws SQLException {
+ long historyDays = config.getInteger("database.historyDays");
+ if (historyDays != 0) {
+ Date timeLimit = new Date(System.currentTimeMillis() - historyDays * 24 * 3600 * 1000);
+ LOGGER.info("Clearing history earlier than " + DateUtil.formatDate(timeLimit, false));
+ QueryBuilder.create(dataSource, getQuery("database.deletePositions"))
+ .setDate("serverTime", timeLimit)
+ .executeUpdate();
+ QueryBuilder.create(dataSource, getQuery("database.deleteEvents"))
+ .setDate("serverTime", timeLimit)
+ .executeUpdate();
+ }
+ }
+
+ public Server getServer() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT_ALL, Server.class))
+ .executeQuerySingle(Server.class);
+ }
+
+ public Collection<Event> getEvents(long deviceId, Date from, Date to) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectEvents"))
+ .setLong("deviceId", deviceId)
+ .setDate("from", from)
+ .setDate("to", to)
+ .executeQuery(Event.class);
+ }
+
+ public Collection<Statistics> getStatistics(Date from, Date to) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectStatistics"))
+ .setDate("from", from)
+ .setDate("to", to)
+ .executeQuery(Statistics.class);
+ }
+
+ public static Class<?> getClassByName(String name) throws ClassNotFoundException {
+ switch (name.toLowerCase().replace("id", "")) {
+ case "device":
+ return Device.class;
+ case "group":
+ return Group.class;
+ case "user":
+ return User.class;
+ case "manageduser":
+ return ManagedUser.class;
+ case "geofence":
+ return Geofence.class;
+ case "driver":
+ return Driver.class;
+ case "attribute":
+ return Attribute.class;
+ case "calendar":
+ return Calendar.class;
+ case "command":
+ return Command.class;
+ case "maintenance":
+ return Maintenance.class;
+ case "notification":
+ return Notification.class;
+ default:
+ throw new ClassNotFoundException();
+ }
+ }
+
+ private static String makeNameId(Class<?> clazz) {
+ String name = clazz.getSimpleName();
+ return Introspector.decapitalize(name) + (!name.contains("Id") ? "Id" : "");
+ }
+
+ public Collection<Permission> getPermissions(Class<? extends BaseModel> owner, Class<? extends BaseModel> property)
+ throws SQLException, ClassNotFoundException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT_ALL, owner, property))
+ .executePermissionsQuery();
+ }
+
+ public void linkObject(Class<?> owner, long ownerId, Class<?> property, long propertyId, boolean link)
+ throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(link ? ACTION_INSERT : ACTION_DELETE, owner, property))
+ .setLong(makeNameId(owner), ownerId)
+ .setLong(makeNameId(property), propertyId)
+ .executeUpdate();
+ }
+
+ public <T extends BaseModel> T getObject(Class<T> clazz, long entityId) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT, clazz))
+ .setLong("id", entityId)
+ .executeQuerySingle(clazz);
+ }
+
+ public <T extends BaseModel> Collection<T> getObjects(Class<T> clazz) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery(ACTION_SELECT_ALL, clazz))
+ .executeQuery(clazz);
+ }
+
+ public void addObject(BaseModel entity) throws SQLException {
+ entity.setId(QueryBuilder.create(dataSource, getQuery(ACTION_INSERT, entity.getClass()), true)
+ .setObject(entity)
+ .executeUpdate());
+ }
+
+ public void updateObject(BaseModel entity) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(ACTION_UPDATE, entity.getClass()))
+ .setObject(entity)
+ .executeUpdate();
+ if (entity instanceof User && ((User) entity).getHashedPassword() != null) {
+ QueryBuilder.create(dataSource, getQuery(ACTION_UPDATE, User.class, true))
+ .setObject(entity)
+ .executeUpdate();
+ }
+ }
+
+ public void removeObject(Class<? extends BaseModel> clazz, long entityId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery(ACTION_DELETE, clazz))
+ .setLong("id", entityId)
+ .executeUpdate();
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/DeviceManager.java b/src/main/java/org/traccar/database/DeviceManager.java
new file mode 100644
index 000000000..de4607d1f
--- /dev/null
+++ b/src/main/java/org/traccar/database/DeviceManager.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.DeviceState;
+import org.traccar.model.DeviceAccumulators;
+import org.traccar.model.Group;
+import org.traccar.model.Position;
+import org.traccar.model.Server;
+
+public class DeviceManager extends BaseObjectManager<Device> implements IdentityManager, ManagableObjects {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DeviceManager.class);
+
+ public static final long DEFAULT_REFRESH_DELAY = 300;
+
+ private final Config config;
+ private final long dataRefreshDelay;
+ private boolean lookupGroupsAttribute;
+
+ private Map<String, Device> devicesByUniqueId;
+ private Map<String, Device> devicesByPhone;
+ private AtomicLong devicesLastUpdate = new AtomicLong();
+
+ private final Map<Long, Position> positions = new ConcurrentHashMap<>();
+
+ private final Map<Long, DeviceState> deviceStates = new ConcurrentHashMap<>();
+
+ public DeviceManager(DataManager dataManager) {
+ super(dataManager, Device.class);
+ this.config = Context.getConfig();
+ if (devicesByPhone == null) {
+ devicesByPhone = new ConcurrentHashMap<>();
+ }
+ if (devicesByUniqueId == null) {
+ devicesByUniqueId = new ConcurrentHashMap<>();
+ }
+ dataRefreshDelay = config.getLong("database.refreshDelay", DEFAULT_REFRESH_DELAY) * 1000;
+ lookupGroupsAttribute = config.getBoolean("deviceManager.lookupGroupsAttribute");
+ refreshLastPositions();
+ }
+
+ @Override
+ public long addUnknownDevice(String uniqueId) {
+ Device device = new Device();
+ device.setName(uniqueId);
+ device.setUniqueId(uniqueId);
+ device.setCategory(Context.getConfig().getString("database.registerUnknown.defaultCategory"));
+
+ long defaultGroupId = Context.getConfig().getLong("database.registerUnknown.defaultGroupId");
+ if (defaultGroupId != 0) {
+ device.setGroupId(defaultGroupId);
+ }
+
+ try {
+ addItem(device);
+
+ LOGGER.info("Automatically registered device " + uniqueId);
+
+ if (defaultGroupId != 0) {
+ Context.getPermissionsManager().refreshDeviceAndGroupPermissions();
+ Context.getPermissionsManager().refreshAllExtendedPermissions();
+ }
+
+ return device.getId();
+ } catch (SQLException e) {
+ LOGGER.warn("Automatic device registration error", e);
+ return 0;
+ }
+ }
+
+ public void updateDeviceCache(boolean force) throws SQLException {
+ long lastUpdate = devicesLastUpdate.get();
+ if ((force || System.currentTimeMillis() - lastUpdate > dataRefreshDelay)
+ && devicesLastUpdate.compareAndSet(lastUpdate, System.currentTimeMillis())) {
+ refreshItems();
+ }
+ }
+
+ @Override
+ public Device getByUniqueId(String uniqueId) throws SQLException {
+ boolean forceUpdate = !devicesByUniqueId.containsKey(uniqueId) && !config.getBoolean("database.ignoreUnknown");
+
+ updateDeviceCache(forceUpdate);
+
+ return devicesByUniqueId.get(uniqueId);
+ }
+
+ public Device getDeviceByPhone(String phone) {
+ return devicesByPhone.get(phone);
+ }
+
+ @Override
+ public Set<Long> getAllItems() {
+ Set<Long> result = super.getAllItems();
+ if (result.isEmpty()) {
+ try {
+ updateDeviceCache(true);
+ } catch (SQLException e) {
+ LOGGER.warn("Update device cache error", e);
+ }
+ result = super.getAllItems();
+ }
+ return result;
+ }
+
+ public Collection<Device> getAllDevices() {
+ return getItems(getAllItems());
+ }
+
+ public Set<Long> getAllUserItems(long userId) {
+ return Context.getPermissionsManager().getDevicePermissions(userId);
+ }
+
+ @Override
+ public Set<Long> getUserItems(long userId) {
+ if (Context.getPermissionsManager() != null) {
+ Set<Long> result = new HashSet<>();
+ for (long deviceId : Context.getPermissionsManager().getDevicePermissions(userId)) {
+ Device device = getById(deviceId);
+ if (device != null && !device.getDisabled()) {
+ result.add(deviceId);
+ }
+ }
+ return result;
+ } else {
+ return new HashSet<>();
+ }
+ }
+
+ public Set<Long> getAllManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getAllUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getAllUserItems(managedUserId));
+ }
+ return result;
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getUserItems(managedUserId));
+ }
+ return result;
+ }
+
+ private void putUniqueDeviceId(Device device) {
+ if (devicesByUniqueId == null) {
+ devicesByUniqueId = new ConcurrentHashMap<>(getAllItems().size());
+ }
+ devicesByUniqueId.put(device.getUniqueId(), device);
+ }
+
+ private void putPhone(Device device) {
+ if (devicesByPhone == null) {
+ devicesByPhone = new ConcurrentHashMap<>(getAllItems().size());
+ }
+ devicesByPhone.put(device.getPhone(), device);
+ }
+
+ @Override
+ protected void addNewItem(Device device) {
+ super.addNewItem(device);
+ putUniqueDeviceId(device);
+ if (device.getPhone() != null && !device.getPhone().isEmpty()) {
+ putPhone(device);
+ }
+ if (Context.getGeofenceManager() != null) {
+ Position lastPosition = getLastPosition(device.getId());
+ if (lastPosition != null) {
+ device.setGeofenceIds(Context.getGeofenceManager().getCurrentDeviceGeofences(lastPosition));
+ }
+ }
+ }
+
+ @Override
+ protected void updateCachedItem(Device device) {
+ Device cachedDevice = getById(device.getId());
+ cachedDevice.setName(device.getName());
+ cachedDevice.setGroupId(device.getGroupId());
+ cachedDevice.setCategory(device.getCategory());
+ cachedDevice.setContact(device.getContact());
+ cachedDevice.setModel(device.getModel());
+ cachedDevice.setDisabled(device.getDisabled());
+ cachedDevice.setAttributes(device.getAttributes());
+ if (!device.getUniqueId().equals(cachedDevice.getUniqueId())) {
+ devicesByUniqueId.remove(cachedDevice.getUniqueId());
+ cachedDevice.setUniqueId(device.getUniqueId());
+ putUniqueDeviceId(cachedDevice);
+ }
+ if (device.getPhone() != null && !device.getPhone().isEmpty()
+ && !device.getPhone().equals(cachedDevice.getPhone())) {
+ String phone = cachedDevice.getPhone();
+ if (phone != null && !phone.isEmpty()) {
+ devicesByPhone.remove(phone);
+ }
+ cachedDevice.setPhone(device.getPhone());
+ putPhone(cachedDevice);
+ }
+ }
+
+ @Override
+ protected void removeCachedItem(long deviceId) {
+ Device cachedDevice = getById(deviceId);
+ if (cachedDevice != null) {
+ String deviceUniqueId = cachedDevice.getUniqueId();
+ String phone = cachedDevice.getPhone();
+ super.removeCachedItem(deviceId);
+ devicesByUniqueId.remove(deviceUniqueId);
+ if (phone != null && !phone.isEmpty()) {
+ devicesByPhone.remove(phone);
+ }
+ }
+ positions.remove(deviceId);
+ }
+
+ public void updateDeviceStatus(Device device) throws SQLException {
+ getDataManager().updateDeviceStatus(device);
+ Device cachedDevice = getById(device.getId());
+ if (cachedDevice != null) {
+ cachedDevice.setStatus(device.getStatus());
+ }
+ }
+
+ private void refreshLastPositions() {
+ if (getDataManager() != null) {
+ try {
+ for (Position position : getDataManager().getLatestPositions()) {
+ positions.put(position.getDeviceId(), position);
+ }
+ } catch (SQLException error) {
+ LOGGER.warn("Load latest positions error", error);
+ }
+ }
+ }
+
+ public boolean isLatestPosition(Position position) {
+ Position lastPosition = getLastPosition(position.getDeviceId());
+ return lastPosition == null || position.getFixTime().compareTo(lastPosition.getFixTime()) >= 0;
+ }
+
+ public void updateLatestPosition(Position position) throws SQLException {
+
+ if (isLatestPosition(position)) {
+
+ getDataManager().updateLatestPosition(position);
+
+ Device device = getById(position.getDeviceId());
+ if (device != null) {
+ device.setPositionId(position.getId());
+ }
+
+ positions.put(position.getDeviceId(), position);
+
+ if (Context.getConnectionManager() != null) {
+ Context.getConnectionManager().updatePosition(position);
+ }
+ }
+ }
+
+ @Override
+ public Position getLastPosition(long deviceId) {
+ return positions.get(deviceId);
+ }
+
+ public Collection<Position> getInitialState(long userId) {
+
+ List<Position> result = new LinkedList<>();
+
+ if (Context.getPermissionsManager() != null) {
+ for (long deviceId : Context.getPermissionsManager().getUserAdmin(userId)
+ ? getAllUserItems(userId) : getUserItems(userId)) {
+ if (positions.containsKey(deviceId)) {
+ result.add(positions.get(deviceId));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean lookupAttributeBoolean(
+ long deviceId, String attributeName, boolean defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Boolean.parseBoolean((String) result) : (Boolean) result;
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public String lookupAttributeString(
+ long deviceId, String attributeName, String defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ return result != null ? (String) result : defaultValue;
+ }
+
+ @Override
+ public int lookupAttributeInteger(long deviceId, String attributeName, int defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Integer.parseInt((String) result) : ((Number) result).intValue();
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public long lookupAttributeLong(
+ long deviceId, String attributeName, long defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Long.parseLong((String) result) : ((Number) result).longValue();
+ }
+ return defaultValue;
+ }
+
+ public double lookupAttributeDouble(
+ long deviceId, String attributeName, double defaultValue, boolean lookupConfig) {
+ Object result = lookupAttribute(deviceId, attributeName, lookupConfig);
+ if (result != null) {
+ return result instanceof String ? Double.parseDouble((String) result) : ((Number) result).doubleValue();
+ }
+ return defaultValue;
+ }
+
+ private Object lookupAttribute(long deviceId, String attributeName, boolean lookupConfig) {
+ Object result = null;
+ Device device = getById(deviceId);
+ if (device != null) {
+ result = device.getAttributes().get(attributeName);
+ if (result == null && lookupGroupsAttribute) {
+ long groupId = device.getGroupId();
+ while (groupId != 0) {
+ Group group = Context.getGroupsManager().getById(groupId);
+ if (group != null) {
+ result = group.getAttributes().get(attributeName);
+ if (result != null) {
+ break;
+ }
+ groupId = group.getGroupId();
+ } else {
+ groupId = 0;
+ }
+ }
+ }
+ if (result == null) {
+ if (lookupConfig) {
+ result = Context.getConfig().getString(attributeName);
+ } else {
+ Server server = Context.getPermissionsManager().getServer();
+ result = server.getAttributes().get(attributeName);
+ }
+ }
+ }
+ return result;
+ }
+
+ public void resetDeviceAccumulators(DeviceAccumulators deviceAccumulators) throws SQLException {
+ Position last = positions.get(deviceAccumulators.getDeviceId());
+ if (last != null) {
+ if (deviceAccumulators.getTotalDistance() != null) {
+ last.getAttributes().put(Position.KEY_TOTAL_DISTANCE, deviceAccumulators.getTotalDistance());
+ }
+ if (deviceAccumulators.getHours() != null) {
+ last.getAttributes().put(Position.KEY_HOURS, deviceAccumulators.getHours());
+ }
+ getDataManager().addObject(last);
+ updateLatestPosition(last);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public DeviceState getDeviceState(long deviceId) {
+ DeviceState deviceState = deviceStates.get(deviceId);
+ if (deviceState == null) {
+ deviceState = new DeviceState();
+ deviceStates.put(deviceId, deviceState);
+ }
+ return deviceState;
+ }
+
+ public void setDeviceState(long deviceId, DeviceState deviceState) {
+ deviceStates.put(deviceId, deviceState);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/DriversManager.java b/src/main/java/org/traccar/database/DriversManager.java
new file mode 100644
index 000000000..930951460
--- /dev/null
+++ b/src/main/java/org/traccar/database/DriversManager.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.traccar.model.Driver;
+
+public class DriversManager extends ExtendedObjectManager<Driver> {
+
+ private Map<String, Driver> driversByUniqueId;
+
+ public DriversManager(DataManager dataManager) {
+ super(dataManager, Driver.class);
+ if (driversByUniqueId == null) {
+ driversByUniqueId = new ConcurrentHashMap<>();
+ }
+ }
+
+ private void putUniqueDriverId(Driver driver) {
+ if (driversByUniqueId == null) {
+ driversByUniqueId = new ConcurrentHashMap<>(getAllItems().size());
+ }
+ driversByUniqueId.put(driver.getUniqueId(), driver);
+ }
+
+ @Override
+ protected void addNewItem(Driver driver) {
+ super.addNewItem(driver);
+ putUniqueDriverId(driver);
+ }
+
+ @Override
+ protected void updateCachedItem(Driver driver) {
+ Driver cachedDriver = getById(driver.getId());
+ cachedDriver.setName(driver.getName());
+ if (!driver.getUniqueId().equals(cachedDriver.getUniqueId())) {
+ driversByUniqueId.remove(cachedDriver.getUniqueId());
+ cachedDriver.setUniqueId(driver.getUniqueId());
+ putUniqueDriverId(cachedDriver);
+ }
+ cachedDriver.setAttributes(driver.getAttributes());
+ }
+
+ @Override
+ protected void removeCachedItem(long driverId) {
+ Driver cachedDriver = getById(driverId);
+ if (cachedDriver != null) {
+ String driverUniqueId = cachedDriver.getUniqueId();
+ super.removeCachedItem(driverId);
+ driversByUniqueId.remove(driverUniqueId);
+ }
+ }
+
+ public Driver getDriverByUniqueId(String uniqueId) {
+ return driversByUniqueId.get(uniqueId);
+ }
+}
diff --git a/src/main/java/org/traccar/database/ExtendedObjectManager.java b/src/main/java/org/traccar/database/ExtendedObjectManager.java
new file mode 100644
index 000000000..ceb85b537
--- /dev/null
+++ b/src/main/java/org/traccar/database/ExtendedObjectManager.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.model.Permission;
+import org.traccar.model.BaseModel;
+
+public abstract class ExtendedObjectManager<T extends BaseModel> extends SimpleObjectManager<T> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExtendedObjectManager.class);
+
+ private final Map<Long, Set<Long>> deviceItems = new ConcurrentHashMap<>();
+ private final Map<Long, Set<Long>> deviceItemsWithGroups = new ConcurrentHashMap<>();
+ private final Map<Long, Set<Long>> groupItems = new ConcurrentHashMap<>();
+
+ protected ExtendedObjectManager(DataManager dataManager, Class<T> baseClass) {
+ super(dataManager, baseClass);
+ refreshExtendedPermissions();
+ }
+
+ public final Set<Long> getGroupItems(long groupId) {
+ if (!groupItems.containsKey(groupId)) {
+ groupItems.put(groupId, new HashSet<Long>());
+ }
+ return groupItems.get(groupId);
+ }
+
+ public final Set<Long> getDeviceItems(long deviceId) {
+ if (!deviceItems.containsKey(deviceId)) {
+ deviceItems.put(deviceId, new HashSet<Long>());
+ }
+ return deviceItems.get(deviceId);
+ }
+
+ public Set<Long> getAllDeviceItems(long deviceId) {
+ if (!deviceItemsWithGroups.containsKey(deviceId)) {
+ deviceItemsWithGroups.put(deviceId, new HashSet<Long>());
+ }
+ return deviceItemsWithGroups.get(deviceId);
+ }
+
+ @Override
+ public void removeItem(long itemId) throws SQLException {
+ super.removeItem(itemId);
+ refreshExtendedPermissions();
+ }
+
+ public void refreshExtendedPermissions() {
+ if (getDataManager() != null) {
+ try {
+
+ Collection<Permission> databaseGroupPermissions =
+ getDataManager().getPermissions(Group.class, getBaseClass());
+
+ groupItems.clear();
+ for (Permission groupPermission : databaseGroupPermissions) {
+ getGroupItems(groupPermission.getOwnerId()).add(groupPermission.getPropertyId());
+ }
+
+ Collection<Permission> databaseDevicePermissions =
+ getDataManager().getPermissions(Device.class, getBaseClass());
+
+ deviceItems.clear();
+ deviceItemsWithGroups.clear();
+
+ for (Permission devicePermission : databaseDevicePermissions) {
+ getDeviceItems(devicePermission.getOwnerId()).add(devicePermission.getPropertyId());
+ getAllDeviceItems(devicePermission.getOwnerId()).add(devicePermission.getPropertyId());
+ }
+
+ for (Device device : Context.getDeviceManager().getAllDevices()) {
+ long groupId = device.getGroupId();
+ while (groupId != 0) {
+ getAllDeviceItems(device.getId()).addAll(getGroupItems(groupId));
+ Group group = Context.getGroupsManager().getById(groupId);
+ if (group != null) {
+ groupId = group.getGroupId();
+ } else {
+ groupId = 0;
+ }
+ }
+ }
+
+ } catch (SQLException | ClassNotFoundException error) {
+ LOGGER.warn("Refresh permissions error", error);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/database/GeofenceManager.java b/src/main/java/org/traccar/database/GeofenceManager.java
new file mode 100644
index 000000000..a32847cf9
--- /dev/null
+++ b/src/main/java/org/traccar/database/GeofenceManager.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.traccar.Context;
+import org.traccar.model.Device;
+import org.traccar.model.Geofence;
+import org.traccar.model.Position;
+
+public class GeofenceManager extends ExtendedObjectManager<Geofence> {
+
+ public GeofenceManager(DataManager dataManager) {
+ super(dataManager, Geofence.class);
+ }
+
+ @Override
+ public final void refreshExtendedPermissions() {
+ super.refreshExtendedPermissions();
+ recalculateDevicesGeofences();
+ }
+
+ public List<Long> getCurrentDeviceGeofences(Position position) {
+ List<Long> result = new ArrayList<>();
+ for (long geofenceId : getAllDeviceItems(position.getDeviceId())) {
+ Geofence geofence = getById(geofenceId);
+ if (geofence != null && geofence.getGeometry()
+ .containsPoint(position.getLatitude(), position.getLongitude())) {
+ result.add(geofenceId);
+ }
+ }
+ return result;
+ }
+
+ public void recalculateDevicesGeofences() {
+ for (Device device : Context.getDeviceManager().getAllDevices()) {
+ List<Long> deviceGeofenceIds = device.getGeofenceIds();
+ if (deviceGeofenceIds == null) {
+ deviceGeofenceIds = new ArrayList<>();
+ } else {
+ deviceGeofenceIds.clear();
+ }
+ Position lastPosition = Context.getIdentityManager().getLastPosition(device.getId());
+ if (lastPosition != null && getAllDeviceItems(device.getId()) != null) {
+ deviceGeofenceIds.addAll(getCurrentDeviceGeofences(lastPosition));
+ }
+ device.setGeofenceIds(deviceGeofenceIds);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/GroupTree.java b/src/main/java/org/traccar/database/GroupTree.java
new file mode 100644
index 000000000..8798f55bc
--- /dev/null
+++ b/src/main/java/org/traccar/database/GroupTree.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class GroupTree {
+
+ private static class TreeNode {
+
+ private Group group;
+ private Device device;
+ private Collection<TreeNode> children = new HashSet<>();
+
+ TreeNode(Group group) {
+ this.group = group;
+ }
+
+ TreeNode(Device device) {
+ this.device = device;
+ }
+
+ @Override
+ public int hashCode() {
+ if (group != null) {
+ return (int) group.getId();
+ } else {
+ return (int) device.getId();
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TreeNode)) {
+ return false;
+ }
+ TreeNode other = (TreeNode) obj;
+ if (other == this) {
+ return true;
+ }
+ if (group != null && other.group != null) {
+ return group.getId() == other.group.getId();
+ } else if (device != null && other.device != null) {
+ return device.getId() == other.device.getId();
+ }
+ return false;
+ }
+
+ public Group getGroup() {
+ return group;
+ }
+
+ public Device getDevice() {
+ return device;
+ }
+
+ public void setParent(TreeNode parent) {
+ if (parent != null) {
+ parent.children.add(this);
+ }
+ }
+
+ public Collection<TreeNode> getChildren() {
+ return children;
+ }
+
+ }
+
+ private final Map<Long, TreeNode> groupMap = new HashMap<>();
+
+ public GroupTree(Collection<Group> groups, Collection<Device> devices) {
+
+ for (Group group : groups) {
+ groupMap.put(group.getId(), new TreeNode(group));
+ }
+
+ for (TreeNode node : groupMap.values()) {
+ if (node.getGroup().getGroupId() != 0) {
+ node.setParent(groupMap.get(node.getGroup().getGroupId()));
+ }
+ }
+
+ Map<Long, TreeNode> deviceMap = new HashMap<>();
+
+ for (Device device : devices) {
+ deviceMap.put(device.getId(), new TreeNode(device));
+ }
+
+ for (TreeNode node : deviceMap.values()) {
+ if (node.getDevice().getGroupId() != 0) {
+ node.setParent(groupMap.get(node.getDevice().getGroupId()));
+ }
+ }
+
+ }
+
+ public Collection<Group> getGroups(long groupId) {
+ Set<TreeNode> results = new HashSet<>();
+ getNodes(results, groupMap.get(groupId));
+ Collection<Group> groups = new ArrayList<>();
+ for (TreeNode node : results) {
+ if (node.getGroup() != null) {
+ groups.add(node.getGroup());
+ }
+ }
+ return groups;
+ }
+
+ public Collection<Device> getDevices(long groupId) {
+ Set<TreeNode> results = new HashSet<>();
+ getNodes(results, groupMap.get(groupId));
+ Collection<Device> devices = new ArrayList<>();
+ for (TreeNode node : results) {
+ if (node.getDevice() != null) {
+ devices.add(node.getDevice());
+ }
+ }
+ return devices;
+ }
+
+ private void getNodes(Set<TreeNode> results, TreeNode node) {
+ if (node != null) {
+ for (TreeNode child : node.getChildren()) {
+ results.add(child);
+ getNodes(results, child);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/GroupsManager.java b/src/main/java/org/traccar/database/GroupsManager.java
new file mode 100644
index 000000000..d8404c614
--- /dev/null
+++ b/src/main/java/org/traccar/database/GroupsManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Group;
+
+public class GroupsManager extends BaseObjectManager<Group> implements ManagableObjects {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GroupsManager.class);
+
+ private AtomicLong groupsLastUpdate = new AtomicLong();
+ private final long dataRefreshDelay;
+
+ public GroupsManager(DataManager dataManager) {
+ super(dataManager, Group.class);
+ dataRefreshDelay = Context.getConfig().getLong("database.refreshDelay",
+ DeviceManager.DEFAULT_REFRESH_DELAY) * 1000;
+ }
+
+ private void checkGroupCycles(Group group) {
+ Set<Long> groups = new HashSet<>();
+ while (group != null) {
+ if (groups.contains(group.getId())) {
+ throw new IllegalArgumentException("Cycle in group hierarchy");
+ }
+ groups.add(group.getId());
+ group = getById(group.getGroupId());
+ }
+ }
+
+ public void updateGroupCache(boolean force) throws SQLException {
+ long lastUpdate = groupsLastUpdate.get();
+ if ((force || System.currentTimeMillis() - lastUpdate > dataRefreshDelay)
+ && groupsLastUpdate.compareAndSet(lastUpdate, System.currentTimeMillis())) {
+ refreshItems();
+ }
+ }
+
+ @Override
+ public Set<Long> getAllItems() {
+ Set<Long> result = super.getAllItems();
+ if (result.isEmpty()) {
+ try {
+ updateGroupCache(true);
+ } catch (SQLException e) {
+ LOGGER.warn("Update group cache error", e);
+ }
+ result = super.getAllItems();
+ }
+ return result;
+ }
+
+ @Override
+ protected void addNewItem(Group group) {
+ checkGroupCycles(group);
+ super.addNewItem(group);
+ }
+
+ @Override
+ public void updateItem(Group group) throws SQLException {
+ checkGroupCycles(group);
+ super.updateItem(group);
+ }
+
+ @Override
+ public Set<Long> getUserItems(long userId) {
+ if (Context.getPermissionsManager() != null) {
+ return Context.getPermissionsManager().getGroupPermissions(userId);
+ } else {
+ return new HashSet<>();
+ }
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getUserItems(managedUserId));
+ }
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/IdentityManager.java b/src/main/java/org/traccar/database/IdentityManager.java
new file mode 100644
index 000000000..6228a0f75
--- /dev/null
+++ b/src/main/java/org/traccar/database/IdentityManager.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+
+public interface IdentityManager {
+
+ long addUnknownDevice(String uniqueId);
+
+ Device getById(long id);
+
+ Device getByUniqueId(String uniqueId) throws Exception;
+
+ Position getLastPosition(long deviceId);
+
+ boolean isLatestPosition(Position position);
+
+ boolean lookupAttributeBoolean(long deviceId, String attributeName, boolean defaultValue, boolean lookupConfig);
+
+ String lookupAttributeString(long deviceId, String attributeName, String defaultValue, boolean lookupConfig);
+
+ int lookupAttributeInteger(long deviceId, String attributeName, int defaultValue, boolean lookupConfig);
+
+ long lookupAttributeLong(long deviceId, String attributeName, long defaultValue, boolean lookupConfig);
+
+ double lookupAttributeDouble(long deviceId, String attributeName, double defaultValue, boolean lookupConfig);
+
+}
diff --git a/src/main/java/org/traccar/database/LdapProvider.java b/src/main/java/org/traccar/database/LdapProvider.java
new file mode 100644
index 000000000..d8b5c9f52
--- /dev/null
+++ b/src/main/java/org/traccar/database/LdapProvider.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.model.User;
+
+import java.util.Hashtable;
+
+public class LdapProvider {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LdapProvider.class);
+
+ private String url;
+ private String searchBase;
+ private String idAttribute;
+ private String nameAttribute;
+ private String mailAttribute;
+ private String searchFilter;
+ private String adminFilter;
+ private String serviceUser;
+ private String servicePassword;
+
+ public LdapProvider(Config config) {
+ String url = config.getString("ldap.url");
+ if (url != null) {
+ this.url = url;
+ } else {
+ this.url = "ldap://" + config.getString("ldap.server") + ":" + config.getInteger("ldap.port", 389);
+ }
+ this.searchBase = config.getString("ldap.base");
+ this.idAttribute = config.getString("ldap.idAttribute", "uid");
+ this.nameAttribute = config.getString("ldap.nameAttribute", "cn");
+ this.mailAttribute = config.getString("ldap.mailAttribute", "mail");
+ this.searchFilter = config.getString("ldap.searchFilter", "(" + idAttribute + "=:login)");
+ String adminGroup = config.getString("ldap.adminGroup");
+ this.adminFilter = config.getString("ldap.adminFilter");
+ if (this.adminFilter == null && adminGroup != null) {
+ this.adminFilter = "(&(" + idAttribute + "=:login)(memberOf=" + adminGroup + "))";
+ }
+ this.serviceUser = config.getString("ldap.user");
+ this.servicePassword = config.getString("ldap.password");
+ }
+
+ private InitialDirContext auth(String accountName, String password) throws NamingException {
+ Hashtable<String, String> env = new Hashtable<>();
+ env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put(Context.PROVIDER_URL, url);
+
+ env.put(Context.SECURITY_AUTHENTICATION, "simple");
+ env.put(Context.SECURITY_PRINCIPAL, accountName);
+ env.put(Context.SECURITY_CREDENTIALS, password);
+
+ return new InitialDirContext(env);
+ }
+
+ private boolean isAdmin(String accountName) {
+ if (this.adminFilter != null) {
+ try {
+ InitialDirContext context = initContext();
+ String searchString = adminFilter.replace(":login", accountName);
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ NamingEnumeration<SearchResult> results = context.search(searchBase, searchString, searchControls);
+ if (results.hasMoreElements()) {
+ results.nextElement();
+ if (results.hasMoreElements()) {
+ LOGGER.warn("Matched multiple users for the accountName: " + accountName);
+ return false;
+ }
+ return true;
+ }
+ } catch (NamingException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public InitialDirContext initContext() throws NamingException {
+ return auth(serviceUser, servicePassword);
+ }
+
+ private SearchResult lookupUser(String accountName) throws NamingException {
+ InitialDirContext context = initContext();
+
+ String searchString = searchFilter.replace(":login", accountName);
+
+ SearchControls searchControls = new SearchControls();
+ String[] attributeFilter = {idAttribute, nameAttribute, mailAttribute};
+ searchControls.setReturningAttributes(attributeFilter);
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+ NamingEnumeration<SearchResult> results = context.search(searchBase, searchString, searchControls);
+
+ SearchResult searchResult = null;
+ if (results.hasMoreElements()) {
+ searchResult = results.nextElement();
+ if (results.hasMoreElements()) {
+ LOGGER.warn("Matched multiple users for the accountName: " + accountName);
+ return null;
+ }
+ }
+
+ return searchResult;
+ }
+
+ public User getUser(String accountName) {
+ SearchResult ldapUser;
+ User user = new User();
+ try {
+ ldapUser = lookupUser(accountName);
+ if (ldapUser != null) {
+ Attribute attribute = ldapUser.getAttributes().get(idAttribute);
+ if (attribute != null) {
+ user.setLogin((String) attribute.get());
+ } else {
+ user.setLogin(accountName);
+ }
+ attribute = ldapUser.getAttributes().get(nameAttribute);
+ if (attribute != null) {
+ user.setName((String) attribute.get());
+ } else {
+ user.setName(accountName);
+ }
+ attribute = ldapUser.getAttributes().get(mailAttribute);
+ if (attribute != null) {
+ user.setEmail((String) attribute.get());
+ } else {
+ user.setEmail(accountName);
+ }
+ }
+ user.setAdministrator(isAdmin(accountName));
+ } catch (NamingException e) {
+ user.setLogin(accountName);
+ user.setName(accountName);
+ user.setEmail(accountName);
+ LOGGER.warn("User lookup error", e);
+ }
+ return user;
+ }
+
+ public boolean login(String username, String password) {
+ try {
+ SearchResult ldapUser = lookupUser(username);
+ if (ldapUser != null) {
+ auth(ldapUser.getNameInNamespace(), password).close();
+ return true;
+ }
+ } catch (NamingException e) {
+ return false;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/MailManager.java b/src/main/java/org/traccar/database/MailManager.java
new file mode 100644
index 000000000..8a2f002cd
--- /dev/null
+++ b/src/main/java/org/traccar/database/MailManager.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 - 2018 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.Main;
+import org.traccar.model.User;
+import org.traccar.notification.PropertiesProvider;
+
+import javax.mail.BodyPart;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Multipart;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import java.util.Date;
+import java.util.Properties;
+
+public final class MailManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MailManager.class);
+
+ private static Properties getProperties(PropertiesProvider provider) {
+ Properties properties = new Properties();
+ String host = provider.getString("mail.smtp.host");
+ if (host != null) {
+ properties.put("mail.transport.protocol", provider.getString("mail.transport.protocol", "smtp"));
+ properties.put("mail.smtp.host", host);
+ properties.put("mail.smtp.port", String.valueOf(provider.getInteger("mail.smtp.port", 25)));
+
+ Boolean starttlsEnable = provider.getBoolean("mail.smtp.starttls.enable");
+ if (starttlsEnable != null) {
+ properties.put("mail.smtp.starttls.enable", String.valueOf(starttlsEnable));
+ }
+ Boolean starttlsRequired = provider.getBoolean("mail.smtp.starttls.required");
+ if (starttlsRequired != null) {
+ properties.put("mail.smtp.starttls.required", String.valueOf(starttlsRequired));
+ }
+
+ Boolean sslEnable = provider.getBoolean("mail.smtp.ssl.enable");
+ if (sslEnable != null) {
+ properties.put("mail.smtp.ssl.enable", String.valueOf(sslEnable));
+ }
+ String sslTrust = provider.getString("mail.smtp.ssl.trust");
+ if (sslTrust != null) {
+ properties.put("mail.smtp.ssl.trust", sslTrust);
+ }
+
+ String sslProtocols = provider.getString("mail.smtp.ssl.protocols");
+ if (sslProtocols != null) {
+ properties.put("mail.smtp.ssl.protocols", sslProtocols);
+ }
+
+ String username = provider.getString("mail.smtp.username");
+ if (username != null) {
+ properties.put("mail.smtp.username", username);
+ }
+ String password = provider.getString("mail.smtp.password");
+ if (password != null) {
+ properties.put("mail.smtp.password", password);
+ }
+ String from = provider.getString("mail.smtp.from");
+ if (from != null) {
+ properties.put("mail.smtp.from", from);
+ }
+ }
+ return properties;
+ }
+
+ public void sendMessage(
+ long userId, String subject, String body) throws MessagingException {
+ sendMessage(userId, subject, body, null);
+ }
+
+ public void sendMessage(
+ long userId, String subject, String body, MimeBodyPart attachment) throws MessagingException {
+ User user = Context.getPermissionsManager().getUser(userId);
+
+ Properties properties = null;
+ if (!Context.getConfig().getBoolean("mail.smtp.ignoreUserConfig")) {
+ properties = getProperties(new PropertiesProvider(user));
+ }
+ if (properties == null || !properties.containsKey("mail.smtp.host")) {
+ properties = getProperties(new PropertiesProvider(Context.getConfig()));
+ }
+ if (!properties.containsKey("mail.smtp.host")) {
+ LOGGER.warn("No SMTP configuration found");
+ return;
+ }
+
+ Session session = Session.getInstance(properties);
+
+ MimeMessage message = new MimeMessage(session);
+
+ String from = properties.getProperty("mail.smtp.from");
+ if (from != null) {
+ message.setFrom(new InternetAddress(from));
+ }
+
+ message.addRecipient(Message.RecipientType.TO, new InternetAddress(user.getEmail()));
+ message.setSubject(subject);
+ message.setSentDate(new Date());
+
+ if (attachment != null) {
+ Multipart multipart = new MimeMultipart();
+
+ BodyPart messageBodyPart = new MimeBodyPart();
+ messageBodyPart.setContent(body, "text/html; charset=utf-8");
+ multipart.addBodyPart(messageBodyPart);
+ multipart.addBodyPart(attachment);
+
+ message.setContent(multipart);
+ } else {
+ message.setContent(body, "text/html; charset=utf-8");
+ }
+
+ try (Transport transport = session.getTransport()) {
+ Main.getInjector().getInstance(StatisticsManager.class).registerMail();
+ transport.connect(
+ properties.getProperty("mail.smtp.host"),
+ properties.getProperty("mail.smtp.username"),
+ properties.getProperty("mail.smtp.password"));
+ transport.sendMessage(message, message.getAllRecipients());
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/MaintenancesManager.java b/src/main/java/org/traccar/database/MaintenancesManager.java
new file mode 100644
index 000000000..4e266cb78
--- /dev/null
+++ b/src/main/java/org/traccar/database/MaintenancesManager.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2018 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 org.traccar.model.Maintenance;
+
+public class MaintenancesManager extends ExtendedObjectManager<Maintenance> {
+
+ public MaintenancesManager(DataManager dataManager) {
+ super(dataManager, Maintenance.class);
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/ManagableObjects.java b/src/main/java/org/traccar/database/ManagableObjects.java
new file mode 100644
index 000000000..ec9549493
--- /dev/null
+++ b/src/main/java/org/traccar/database/ManagableObjects.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.util.Set;
+
+public interface ManagableObjects {
+
+ Set<Long> getUserItems(long userId);
+
+ Set<Long> getManagedItems(long userId);
+
+}
diff --git a/src/main/java/org/traccar/database/MediaManager.java b/src/main/java/org/traccar/database/MediaManager.java
new file mode 100644
index 000000000..edade5766
--- /dev/null
+++ b/src/main/java/org/traccar/database/MediaManager.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import io.netty.buffer.ByteBuf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class MediaManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MediaManager.class);
+
+ private String path;
+
+ public MediaManager(String path) {
+ this.path = path;
+ }
+
+ private File createFile(String uniqueId, String name) throws IOException {
+ Path filePath = Paths.get(path, uniqueId, name);
+ Path directoryPath = filePath.getParent();
+ if (directoryPath != null) {
+ Files.createDirectories(directoryPath);
+ }
+ return filePath.toFile();
+ }
+
+ public String writeFile(String uniqueId, ByteBuf buf, String extension) {
+ if (path != null) {
+ int size = buf.readableBytes();
+ String name = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "." + extension;
+ try (FileOutputStream output = new FileOutputStream(createFile(uniqueId, name));
+ FileChannel fileChannel = output.getChannel()) {
+ ByteBuffer byteBuffer = buf.nioBuffer();
+ int written = 0;
+ while (written < size) {
+ written += fileChannel.write(byteBuffer);
+ }
+ fileChannel.force(false);
+ return name;
+ } catch (IOException e) {
+ LOGGER.warn("Save media file error", e);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java
new file mode 100644
index 000000000..09df4c571
--- /dev/null
+++ b/src/main/java/org/traccar/database/NotificationManager.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 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.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Calendar;
+import org.traccar.model.Event;
+import org.traccar.model.Notification;
+import org.traccar.model.Position;
+import org.traccar.model.Typed;
+
+public class NotificationManager extends ExtendedObjectManager<Notification> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NotificationManager.class);
+
+ private boolean geocodeOnRequest;
+
+ public NotificationManager(DataManager dataManager) {
+ super(dataManager, Notification.class);
+ geocodeOnRequest = Context.getConfig().getBoolean("geocoder.onRequest");
+ }
+
+ private Set<Long> getEffectiveNotifications(long userId, long deviceId, Date time) {
+ Set<Long> result = new HashSet<>();
+ Set<Long> deviceNotifications = getAllDeviceItems(deviceId);
+ for (long itemId : getUserItems(userId)) {
+ if (getById(itemId).getAlways() || deviceNotifications.contains(itemId)) {
+ long calendarId = getById(itemId).getCalendarId();
+ Calendar calendar = calendarId != 0 ? Context.getCalendarManager().getById(calendarId) : null;
+ if (calendar == null || calendar.checkMoment(time)) {
+ result.add(itemId);
+ }
+ }
+ }
+ return result;
+ }
+
+ public void updateEvent(Event event, Position position) {
+ try {
+ getDataManager().addObject(event);
+ } catch (SQLException error) {
+ LOGGER.warn("Event save error", error);
+ }
+
+ if (position != null && geocodeOnRequest && Context.getGeocoder() != null && position.getAddress() == null) {
+ position.setAddress(Context.getGeocoder()
+ .getAddress(position.getLatitude(), position.getLongitude(), null));
+ }
+
+ long deviceId = event.getDeviceId();
+ Set<Long> users = Context.getPermissionsManager().getDeviceUsers(deviceId);
+ Set<Long> usersToForward = null;
+ if (Context.getEventForwarder() != null) {
+ usersToForward = new HashSet<>();
+ }
+ for (long userId : users) {
+ if ((event.getGeofenceId() == 0
+ || Context.getGeofenceManager().checkItemPermission(userId, event.getGeofenceId()))
+ && (event.getMaintenanceId() == 0
+ || Context.getMaintenancesManager().checkItemPermission(userId, event.getMaintenanceId()))) {
+ if (usersToForward != null) {
+ usersToForward.add(userId);
+ }
+ final Set<String> notificators = new HashSet<>();
+ for (long notificationId : getEffectiveNotifications(userId, deviceId, event.getServerTime())) {
+ Notification notification = getById(notificationId);
+ if (getById(notificationId).getType().equals(event.getType())) {
+ boolean filter = false;
+ if (event.getType().equals(Event.TYPE_ALARM)) {
+ String alarms = notification.getString("alarms");
+ if (alarms == null || !alarms.contains(event.getString(Position.KEY_ALARM))) {
+ filter = true;
+ }
+ }
+ if (!filter) {
+ notificators.addAll(notification.getNotificatorsTypes());
+ }
+ }
+ }
+ for (String notificator : notificators) {
+ Context.getNotificatorManager().getNotificator(notificator).sendAsync(userId, event, position);
+ }
+ }
+ }
+ if (Context.getEventForwarder() != null) {
+ Context.getEventForwarder().forwardEvent(event, position, usersToForward);
+ }
+ }
+
+ public void updateEvents(Map<Event, Position> events) {
+ for (Entry<Event, Position> event : events.entrySet()) {
+ updateEvent(event.getKey(), event.getValue());
+ }
+ }
+
+ public Set<Typed> getAllNotificationTypes() {
+ Set<Typed> types = new HashSet<>();
+ Field[] fields = Event.class.getDeclaredFields();
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getName().startsWith("TYPE_")) {
+ try {
+ types.add(new Typed(field.get(null).toString()));
+ } catch (IllegalArgumentException | IllegalAccessException error) {
+ LOGGER.warn("Get event types error", error);
+ }
+ }
+ }
+ return types;
+ }
+}
diff --git a/src/main/java/org/traccar/database/PermissionsManager.java b/src/main/java/org/traccar/database/PermissionsManager.java
new file mode 100644
index 000000000..ced0df1c0
--- /dev/null
+++ b/src/main/java/org/traccar/database/PermissionsManager.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright 2015 - 2018 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.Attribute;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Calendar;
+import org.traccar.model.Command;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.model.ManagedUser;
+import org.traccar.model.Notification;
+import org.traccar.model.Permission;
+import org.traccar.model.Server;
+import org.traccar.model.User;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class PermissionsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PermissionsManager.class);
+
+ private final DataManager dataManager;
+ private final UsersManager usersManager;
+
+ private volatile Server server;
+
+ private final Map<Long, Set<Long>> groupPermissions = new HashMap<>();
+ private final Map<Long, Set<Long>> devicePermissions = new HashMap<>();
+ private final Map<Long, Set<Long>> deviceUsers = new HashMap<>();
+ private final Map<Long, Set<Long>> groupDevices = new HashMap<>();
+
+ public PermissionsManager(DataManager dataManager, UsersManager usersManager) {
+ this.dataManager = dataManager;
+ this.usersManager = usersManager;
+ refreshServer();
+ refreshDeviceAndGroupPermissions();
+ }
+
+ public User getUser(long userId) {
+ return usersManager.getById(userId);
+ }
+
+ public Set<Long> getGroupPermissions(long userId) {
+ if (!groupPermissions.containsKey(userId)) {
+ groupPermissions.put(userId, new HashSet<>());
+ }
+ return groupPermissions.get(userId);
+ }
+
+ public Set<Long> getDevicePermissions(long userId) {
+ if (!devicePermissions.containsKey(userId)) {
+ devicePermissions.put(userId, new HashSet<>());
+ }
+ return devicePermissions.get(userId);
+ }
+
+ private Set<Long> getAllDeviceUsers(long deviceId) {
+ if (!deviceUsers.containsKey(deviceId)) {
+ deviceUsers.put(deviceId, new HashSet<>());
+ }
+ return deviceUsers.get(deviceId);
+ }
+
+ public Set<Long> getDeviceUsers(long deviceId) {
+ Device device = Context.getIdentityManager().getById(deviceId);
+ if (device != null && !device.getDisabled()) {
+ return getAllDeviceUsers(deviceId);
+ } else {
+ Set<Long> result = new HashSet<>();
+ for (long userId : getAllDeviceUsers(deviceId)) {
+ if (getUserAdmin(userId)) {
+ result.add(userId);
+ }
+ }
+ return result;
+ }
+ }
+
+ public Set<Long> getGroupDevices(long groupId) {
+ if (!groupDevices.containsKey(groupId)) {
+ groupDevices.put(groupId, new HashSet<>());
+ }
+ return groupDevices.get(groupId);
+ }
+
+ public void refreshServer() {
+ try {
+ server = dataManager.getServer();
+ } catch (SQLException error) {
+ LOGGER.warn("Refresh server config error", error);
+ }
+ }
+
+ public final void refreshDeviceAndGroupPermissions() {
+ groupPermissions.clear();
+ devicePermissions.clear();
+ try {
+ GroupTree groupTree = new GroupTree(Context.getGroupsManager().getItems(
+ Context.getGroupsManager().getAllItems()),
+ Context.getDeviceManager().getAllDevices());
+ for (Permission groupPermission : dataManager.getPermissions(User.class, Group.class)) {
+ Set<Long> userGroupPermissions = getGroupPermissions(groupPermission.getOwnerId());
+ Set<Long> userDevicePermissions = getDevicePermissions(groupPermission.getOwnerId());
+ userGroupPermissions.add(groupPermission.getPropertyId());
+ for (Group group : groupTree.getGroups(groupPermission.getPropertyId())) {
+ userGroupPermissions.add(group.getId());
+ }
+ for (Device device : groupTree.getDevices(groupPermission.getPropertyId())) {
+ userDevicePermissions.add(device.getId());
+ }
+ }
+
+ for (Permission devicePermission : dataManager.getPermissions(User.class, Device.class)) {
+ getDevicePermissions(devicePermission.getOwnerId()).add(devicePermission.getPropertyId());
+ }
+
+ groupDevices.clear();
+ for (long groupId : Context.getGroupsManager().getAllItems()) {
+ for (Device device : groupTree.getDevices(groupId)) {
+ getGroupDevices(groupId).add(device.getId());
+ }
+ }
+
+ } catch (SQLException | ClassNotFoundException error) {
+ LOGGER.warn("Refresh device permissions error", error);
+ }
+
+ deviceUsers.clear();
+ for (Map.Entry<Long, Set<Long>> entry : devicePermissions.entrySet()) {
+ for (long deviceId : entry.getValue()) {
+ getAllDeviceUsers(deviceId).add(entry.getKey());
+ }
+ }
+ }
+
+ public boolean getUserAdmin(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getAdministrator();
+ }
+
+ public void checkAdmin(long userId) throws SecurityException {
+ if (!getUserAdmin(userId)) {
+ throw new SecurityException("Admin access required");
+ }
+ }
+
+ public boolean getUserManager(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getUserLimit() != 0;
+ }
+
+ public void checkManager(long userId) throws SecurityException {
+ if (!getUserManager(userId)) {
+ throw new SecurityException("Manager access required");
+ }
+ }
+
+ public void checkManager(long userId, long managedUserId) throws SecurityException {
+ checkManager(userId);
+ if (!usersManager.getUserItems(userId).contains(managedUserId)) {
+ throw new SecurityException("User access denied");
+ }
+ }
+
+ public void checkUserLimit(long userId) throws SecurityException {
+ int userLimit = getUser(userId).getUserLimit();
+ if (userLimit != -1 && usersManager.getUserItems(userId).size() >= userLimit) {
+ throw new SecurityException("Manager user limit reached");
+ }
+ }
+
+ public void checkDeviceLimit(long userId) throws SecurityException {
+ int deviceLimit = getUser(userId).getDeviceLimit();
+ if (deviceLimit != -1) {
+ int deviceCount = 0;
+ if (getUserManager(userId)) {
+ deviceCount = Context.getDeviceManager().getAllManagedItems(userId).size();
+ } else {
+ deviceCount = Context.getDeviceManager().getAllUserItems(userId).size();
+ }
+ if (deviceCount >= deviceLimit) {
+ throw new SecurityException("User device limit reached");
+ }
+ }
+ }
+
+ public boolean getUserReadonly(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getReadonly();
+ }
+
+ public boolean getUserDeviceReadonly(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getDeviceReadonly();
+ }
+
+ public boolean getUserLimitCommands(long userId) {
+ User user = getUser(userId);
+ return user != null && user.getLimitCommands();
+ }
+
+ public void checkReadonly(long userId) throws SecurityException {
+ if (!getUserAdmin(userId) && (server.getReadonly() || getUserReadonly(userId))) {
+ throw new SecurityException("Account is readonly");
+ }
+ }
+
+ public void checkDeviceReadonly(long userId) throws SecurityException {
+ if (!getUserAdmin(userId) && (server.getDeviceReadonly() || getUserDeviceReadonly(userId))) {
+ throw new SecurityException("Account is device readonly");
+ }
+ }
+
+ public void checkLimitCommands(long userId) throws SecurityException {
+ if (!getUserAdmin(userId) && (server.getLimitCommands() || getUserLimitCommands(userId))) {
+ throw new SecurityException("Account has limit sending commands");
+ }
+ }
+
+ public void checkUserDeviceCommand(long userId, long deviceId, long commandId) throws SecurityException {
+ if (!getUserAdmin(userId) && Context.getCommandsManager().checkDeviceCommand(deviceId, commandId)) {
+ throw new SecurityException("Command can not be sent to this device");
+ }
+ }
+
+ public void checkUserEnabled(long userId) throws SecurityException {
+ User user = getUser(userId);
+ if (user == null) {
+ throw new SecurityException("Unknown account");
+ }
+ if (user.getDisabled()) {
+ throw new SecurityException("Account is disabled");
+ }
+ if (user.getExpirationTime() != null && System.currentTimeMillis() > user.getExpirationTime().getTime()) {
+ throw new SecurityException("Account has expired");
+ }
+ }
+
+ public void checkUserUpdate(long userId, User before, User after) throws SecurityException {
+ if (before.getAdministrator() != after.getAdministrator()
+ || before.getDeviceLimit() != after.getDeviceLimit()
+ || before.getUserLimit() != after.getUserLimit()) {
+ checkAdmin(userId);
+ }
+ User user = getUser(userId);
+ if (user != null && user.getExpirationTime() != null
+ && (after.getExpirationTime() == null
+ || user.getExpirationTime().compareTo(after.getExpirationTime()) < 0)) {
+ checkAdmin(userId);
+ }
+ if (before.getReadonly() != after.getReadonly()
+ || before.getDeviceReadonly() != after.getDeviceReadonly()
+ || before.getDisabled() != after.getDisabled()
+ || before.getLimitCommands() != after.getLimitCommands()) {
+ if (userId == after.getId()) {
+ checkAdmin(userId);
+ }
+ if (!getUserAdmin(userId)) {
+ checkManager(userId);
+ }
+ }
+ }
+
+ public void checkUser(long userId, long managedUserId) throws SecurityException {
+ if (userId != managedUserId && !getUserAdmin(userId)) {
+ checkManager(userId, managedUserId);
+ }
+ }
+
+ public void checkGroup(long userId, long groupId) throws SecurityException {
+ if (!getGroupPermissions(userId).contains(groupId) && !getUserAdmin(userId)) {
+ checkManager(userId);
+ for (long managedUserId : usersManager.getUserItems(userId)) {
+ if (getGroupPermissions(managedUserId).contains(groupId)) {
+ return;
+ }
+ }
+ throw new SecurityException("Group access denied");
+ }
+ }
+
+ public void checkDevice(long userId, long deviceId) throws SecurityException {
+ if (!Context.getDeviceManager().getUserItems(userId).contains(deviceId) && !getUserAdmin(userId)) {
+ checkManager(userId);
+ for (long managedUserId : usersManager.getUserItems(userId)) {
+ if (Context.getDeviceManager().getUserItems(managedUserId).contains(deviceId)) {
+ return;
+ }
+ }
+ throw new SecurityException("Device access denied");
+ }
+ }
+
+ public void checkRegistration(long userId) {
+ if (!server.getRegistration() && !getUserAdmin(userId)) {
+ throw new SecurityException("Registration disabled");
+ }
+ }
+
+ public void checkPermission(Class<?> object, long userId, long objectId)
+ throws SecurityException {
+ SimpleObjectManager<? extends BaseModel> manager = null;
+
+ if (object.equals(Device.class)) {
+ checkDevice(userId, objectId);
+ } else if (object.equals(Group.class)) {
+ checkGroup(userId, objectId);
+ } else if (object.equals(User.class) || object.equals(ManagedUser.class)) {
+ checkUser(userId, objectId);
+ } else if (object.equals(Geofence.class)) {
+ manager = Context.getGeofenceManager();
+ } else if (object.equals(Attribute.class)) {
+ manager = Context.getAttributesManager();
+ } else if (object.equals(Driver.class)) {
+ manager = Context.getDriversManager();
+ } else if (object.equals(Calendar.class)) {
+ manager = Context.getCalendarManager();
+ } else if (object.equals(Command.class)) {
+ manager = Context.getCommandsManager();
+ } else if (object.equals(Maintenance.class)) {
+ manager = Context.getMaintenancesManager();
+ } else if (object.equals(Notification.class)) {
+ manager = Context.getNotificationManager();
+ } else {
+ throw new IllegalArgumentException("Unknown object type");
+ }
+
+ if (manager != null && !manager.checkItemPermission(userId, objectId) && !getUserAdmin(userId)) {
+ checkManager(userId);
+ for (long managedUserId : usersManager.getManagedItems(userId)) {
+ if (manager.checkItemPermission(managedUserId, objectId)) {
+ return;
+ }
+ }
+ throw new SecurityException("Type " + object + " access denied");
+ }
+ }
+
+ public void refreshAllUsersPermissions() {
+ if (Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshUserItems();
+ }
+ Context.getCalendarManager().refreshUserItems();
+ Context.getDriversManager().refreshUserItems();
+ Context.getAttributesManager().refreshUserItems();
+ Context.getCommandsManager().refreshUserItems();
+ Context.getMaintenancesManager().refreshUserItems();
+ if (Context.getNotificationManager() != null) {
+ Context.getNotificationManager().refreshUserItems();
+ }
+ }
+
+ public void refreshAllExtendedPermissions() {
+ if (Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshExtendedPermissions();
+ }
+ Context.getDriversManager().refreshExtendedPermissions();
+ Context.getAttributesManager().refreshExtendedPermissions();
+ Context.getCommandsManager().refreshExtendedPermissions();
+ Context.getMaintenancesManager().refreshExtendedPermissions();
+ }
+
+ public void refreshPermissions(Permission permission) {
+ if (permission.getOwnerClass().equals(User.class)) {
+ if (permission.getPropertyClass().equals(Device.class)
+ || permission.getPropertyClass().equals(Group.class)) {
+ refreshDeviceAndGroupPermissions();
+ refreshAllExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(ManagedUser.class)) {
+ usersManager.refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Geofence.class) && Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Driver.class)) {
+ Context.getDriversManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Attribute.class)) {
+ Context.getAttributesManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Calendar.class)) {
+ Context.getCalendarManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Command.class)) {
+ Context.getCommandsManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Maintenance.class)) {
+ Context.getMaintenancesManager().refreshUserItems();
+ } else if (permission.getPropertyClass().equals(Notification.class)
+ && Context.getNotificationManager() != null) {
+ Context.getNotificationManager().refreshUserItems();
+ }
+ } else if (permission.getOwnerClass().equals(Device.class) || permission.getOwnerClass().equals(Group.class)) {
+ if (permission.getPropertyClass().equals(Geofence.class) && Context.getGeofenceManager() != null) {
+ Context.getGeofenceManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Driver.class)) {
+ Context.getDriversManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Attribute.class)) {
+ Context.getAttributesManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Command.class)) {
+ Context.getCommandsManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Maintenance.class)) {
+ Context.getMaintenancesManager().refreshExtendedPermissions();
+ } else if (permission.getPropertyClass().equals(Notification.class)
+ && Context.getNotificationManager() != null) {
+ Context.getNotificationManager().refreshExtendedPermissions();
+ }
+ }
+ }
+
+ public Server getServer() {
+ return server;
+ }
+
+ public void updateServer(Server server) throws SQLException {
+ dataManager.updateObject(server);
+ this.server = server;
+ }
+
+ public User login(String email, String password) throws SQLException {
+ User user = dataManager.login(email, password);
+ if (user != null) {
+ checkUserEnabled(user.getId());
+ return getUser(user.getId());
+ }
+ return null;
+ }
+
+ public Object lookupAttribute(long userId, String key, Object defaultValue) {
+ Object preference;
+ Object serverPreference = server.getAttributes().get(key);
+ Object userPreference = getUser(userId).getAttributes().get(key);
+ if (server.getForceSettings()) {
+ preference = serverPreference != null ? serverPreference : userPreference;
+ } else {
+ preference = userPreference != null ? userPreference : serverPreference;
+ }
+ return preference != null ? preference : defaultValue;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/QueryBuilder.java b/src/main/java/org/traccar/database/QueryBuilder.java
new file mode 100644
index 000000000..5528b2320
--- /dev/null
+++ b/src/main/java/org/traccar/database/QueryBuilder.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.MiscFormatter;
+import org.traccar.model.Permission;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public final class QueryBuilder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(QueryBuilder.class);
+
+ private final Map<String, List<Integer>> indexMap = new HashMap<>();
+ private Connection connection;
+ private PreparedStatement statement;
+ private final String query;
+ private final boolean returnGeneratedKeys;
+
+ private QueryBuilder(DataSource dataSource, String query, boolean returnGeneratedKeys) throws SQLException {
+ this.query = query;
+ this.returnGeneratedKeys = returnGeneratedKeys;
+ if (query != null) {
+ connection = dataSource.getConnection();
+ String parsedQuery = parse(query.trim(), indexMap);
+ try {
+ if (returnGeneratedKeys) {
+ statement = connection.prepareStatement(parsedQuery, Statement.RETURN_GENERATED_KEYS);
+ } else {
+ statement = connection.prepareStatement(parsedQuery);
+ }
+ } catch (SQLException error) {
+ connection.close();
+ throw error;
+ }
+ }
+ }
+
+ private static String parse(String query, Map<String, List<Integer>> paramMap) {
+
+ int length = query.length();
+ StringBuilder parsedQuery = new StringBuilder(length);
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ int index = 1;
+
+ for (int i = 0; i < length; i++) {
+
+ char c = query.charAt(i);
+
+ // String end
+ if (inSingleQuote) {
+ if (c == '\'') {
+ inSingleQuote = false;
+ }
+ } else if (inDoubleQuote) {
+ if (c == '"') {
+ inDoubleQuote = false;
+ }
+ } else {
+
+ // String begin
+ if (c == '\'') {
+ inSingleQuote = true;
+ } else if (c == '"') {
+ inDoubleQuote = true;
+ } else if (c == ':' && i + 1 < length
+ && Character.isJavaIdentifierStart(query.charAt(i + 1))) {
+
+ // Identifier name
+ int j = i + 2;
+ while (j < length && Character.isJavaIdentifierPart(query.charAt(j))) {
+ j++;
+ }
+
+ String name = query.substring(i + 1, j);
+ c = '?';
+ i += name.length();
+ name = name.toLowerCase();
+
+ // Add to list
+ List<Integer> indexList = paramMap.get(name);
+ if (indexList == null) {
+ indexList = new LinkedList<>();
+ paramMap.put(name, indexList);
+ }
+ indexList.add(index);
+
+ index++;
+ }
+ }
+
+ parsedQuery.append(c);
+ }
+
+ return parsedQuery.toString();
+ }
+
+ public static QueryBuilder create(DataSource dataSource, String query) throws SQLException {
+ return new QueryBuilder(dataSource, query, false);
+ }
+
+ public static QueryBuilder create(
+ DataSource dataSource, String query, boolean returnGeneratedKeys) throws SQLException {
+ return new QueryBuilder(dataSource, query, returnGeneratedKeys);
+ }
+
+ private List<Integer> indexes(String name) {
+ name = name.toLowerCase();
+ List<Integer> result = indexMap.get(name);
+ if (result == null) {
+ result = new LinkedList<>();
+ }
+ return result;
+ }
+
+ public QueryBuilder setBoolean(String name, boolean value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setBoolean(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setInteger(String name, int value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setInt(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setLong(String name, long value) throws SQLException {
+ return setLong(name, value, false);
+ }
+
+ public QueryBuilder setLong(String name, long value, boolean nullIfZero) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == 0 && nullIfZero) {
+ statement.setNull(i, Types.INTEGER);
+ } else {
+ statement.setLong(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setDouble(String name, double value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ statement.setDouble(i, value);
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setString(String name, String value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.VARCHAR);
+ } else {
+ statement.setString(i, value);
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder setDate(String name, Date value) throws SQLException {
+ for (int i : indexes(name)) {
+ try {
+ if (value == null) {
+ statement.setNull(i, Types.TIMESTAMP);
+ } else {
+ statement.setTimestamp(i, new Timestamp(value.getTime()));
+ }
+ } catch (SQLException error) {
+ statement.close();
+ connection.close();
+ throw error;
+ }
+ }
+ 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();
+
+ for (Method method : methods) {
+ if (method.getName().startsWith("get") && method.getParameterTypes().length == 0
+ && !method.isAnnotationPresent(QueryIgnore.class)) {
+ String name = method.getName().substring(3);
+ try {
+ if (method.getReturnType().equals(boolean.class)) {
+ setBoolean(name, (Boolean) method.invoke(object));
+ } else if (method.getReturnType().equals(int.class)) {
+ setInteger(name, (Integer) method.invoke(object));
+ } else if (method.getReturnType().equals(long.class)) {
+ setLong(name, (Long) method.invoke(object), name.endsWith("Id"));
+ } else if (method.getReturnType().equals(double.class)) {
+ setDouble(name, (Double) method.invoke(object));
+ } else if (method.getReturnType().equals(String.class)) {
+ 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)
+ && Context.getConfig().getBoolean("database.xml")) {
+ setString(name, MiscFormatter.toXmlString((Map) method.invoke(object)));
+ } else {
+ setString(name, Context.getObjectMapper().writeValueAsString(method.invoke(object)));
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException | JsonProcessingException error) {
+ LOGGER.warn("Get property error", error);
+ }
+ }
+ }
+
+ return this;
+ }
+
+ private interface ResultSetProcessor<T> {
+ void process(T object, ResultSet resultSet) throws SQLException;
+ }
+
+ public <T> T executeQuerySingle(Class<T> clazz) throws SQLException {
+ Collection<T> result = executeQuery(clazz);
+ if (!result.isEmpty()) {
+ return result.iterator().next();
+ } else {
+ return null;
+ }
+ }
+
+ private <T> void addProcessors(
+ List<ResultSetProcessor<T>> processors,
+ final Class<?> parameterType, final Method method, final String name) {
+
+ if (parameterType.equals(boolean.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getBoolean(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(int.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getInt(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(long.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getLong(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(double.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getDouble(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(String.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ method.invoke(object, resultSet.getString(name));
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else if (parameterType.equals(Date.class)) {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ try {
+ Timestamp timestamp = resultSet.getTimestamp(name);
+ if (timestamp != null) {
+ method.invoke(object, new Date(timestamp.getTime()));
+ }
+ } catch (IllegalAccessException | InvocationTargetException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } 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) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ });
+ } else {
+ processors.add(new ResultSetProcessor<T>() {
+ @Override
+ public void process(T object, ResultSet resultSet) throws SQLException {
+ String value = resultSet.getString(name);
+ if (value != null && !value.isEmpty()) {
+ try {
+ method.invoke(object, Context.getObjectMapper().readValue(value, parameterType));
+ } catch (InvocationTargetException | IllegalAccessException | IOException error) {
+ LOGGER.warn("Set property error", error);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public <T> Collection<T> executeQuery(Class<T> clazz) throws SQLException {
+ List<T> result = new LinkedList<>();
+
+ if (query != null) {
+
+ try {
+
+ try (ResultSet resultSet = statement.executeQuery()) {
+
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+
+ List<ResultSetProcessor<T>> processors = new LinkedList<>();
+
+ Method[] methods = clazz.getMethods();
+
+ for (final Method method : methods) {
+ if (method.getName().startsWith("set") && method.getParameterTypes().length == 1
+ && !method.isAnnotationPresent(QueryIgnore.class)) {
+
+ final String name = method.getName().substring(3);
+
+ // Check if column exists
+ boolean column = false;
+ for (int i = 1; i <= resultMetaData.getColumnCount(); i++) {
+ if (name.equalsIgnoreCase(resultMetaData.getColumnLabel(i))) {
+ column = true;
+ break;
+ }
+ }
+ if (!column) {
+ continue;
+ }
+
+ addProcessors(processors, method.getParameterTypes()[0], method, name);
+ }
+ }
+
+ while (resultSet.next()) {
+ try {
+ T object = clazz.newInstance();
+ for (ResultSetProcessor<T> processor : processors) {
+ processor.process(object, resultSet);
+ }
+ result.add(object);
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+
+ return result;
+ }
+
+ public long executeUpdate() throws SQLException {
+
+ if (query != null) {
+ try {
+ statement.execute();
+ if (returnGeneratedKeys) {
+ ResultSet resultSet = statement.getGeneratedKeys();
+ if (resultSet.next()) {
+ return resultSet.getLong(1);
+ }
+ }
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+ return 0;
+ }
+
+ public Collection<Permission> executePermissionsQuery() throws SQLException, ClassNotFoundException {
+ List<Permission> result = new LinkedList<>();
+ if (query != null) {
+ try {
+ try (ResultSet resultSet = statement.executeQuery()) {
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+ while (resultSet.next()) {
+ LinkedHashMap<String, Long> map = new LinkedHashMap<>();
+ for (int i = 1; i <= resultMetaData.getColumnCount(); i++) {
+ String label = resultMetaData.getColumnLabel(i);
+ map.put(label, resultSet.getLong(label));
+ }
+ result.add(new Permission(map));
+ }
+ }
+ } finally {
+ statement.close();
+ connection.close();
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/QueryExtended.java b/src/main/java/org/traccar/database/QueryExtended.java
new file mode 100644
index 000000000..07bc2c211
--- /dev/null
+++ b/src/main/java/org/traccar/database/QueryExtended.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QueryExtended {
+}
diff --git a/src/main/java/org/traccar/database/QueryIgnore.java b/src/main/java/org/traccar/database/QueryIgnore.java
new file mode 100644
index 000000000..ac835cf2f
--- /dev/null
+++ b/src/main/java/org/traccar/database/QueryIgnore.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QueryIgnore {
+}
diff --git a/src/main/java/org/traccar/database/SimpleObjectManager.java b/src/main/java/org/traccar/database/SimpleObjectManager.java
new file mode 100644
index 000000000..15dda4520
--- /dev/null
+++ b/src/main/java/org/traccar/database/SimpleObjectManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.Context;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Permission;
+import org.traccar.model.User;
+
+public abstract class SimpleObjectManager<T extends BaseModel> extends BaseObjectManager<T>
+ implements ManagableObjects {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleObjectManager.class);
+
+ private Map<Long, Set<Long>> userItems;
+
+ protected SimpleObjectManager(DataManager dataManager, Class<T> baseClass) {
+ super(dataManager, baseClass);
+ }
+
+ @Override
+ public final Set<Long> getUserItems(long userId) {
+ if (!userItems.containsKey(userId)) {
+ userItems.put(userId, new HashSet<Long>());
+ }
+ return userItems.get(userId);
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ for (long managedUserId : Context.getUsersManager().getUserItems(userId)) {
+ result.addAll(getUserItems(managedUserId));
+ }
+ return result;
+ }
+
+ public final boolean checkItemPermission(long userId, long itemId) {
+ return getUserItems(userId).contains(itemId);
+ }
+
+ @Override
+ public void refreshItems() {
+ super.refreshItems();
+ refreshUserItems();
+ }
+
+ public final void refreshUserItems() {
+ if (getDataManager() != null) {
+ try {
+ if (userItems != null) {
+ userItems.clear();
+ } else {
+ userItems = new ConcurrentHashMap<>();
+ }
+ for (Permission permission : getDataManager().getPermissions(User.class, getBaseClass())) {
+ getUserItems(permission.getOwnerId()).add(permission.getPropertyId());
+ }
+ } catch (SQLException | ClassNotFoundException error) {
+ LOGGER.warn("Error getting permissions", error);
+ }
+ }
+ }
+
+ @Override
+ public void removeItem(long itemId) throws SQLException {
+ super.removeItem(itemId);
+ refreshUserItems();
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/StatisticsManager.java b/src/main/java/org/traccar/database/StatisticsManager.java
new file mode 100644
index 000000000..e59f8e767
--- /dev/null
+++ b/src/main/java/org/traccar/database/StatisticsManager.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2016 - 2019 Anton Tananaev (anton@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.database;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.DateUtil;
+import org.traccar.model.Statistics;
+
+import javax.inject.Inject;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Form;
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class StatisticsManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsManager.class);
+
+ private static final int SPLIT_MODE = Calendar.DAY_OF_MONTH;
+
+ private final Config config;
+ private final DataManager dataManager;
+ private final Client client;
+
+ private AtomicInteger lastUpdate = new AtomicInteger(Calendar.getInstance().get(SPLIT_MODE));
+
+ private Set<Long> users = new HashSet<>();
+ private Set<Long> devices = new HashSet<>();
+
+ private int requests;
+ private int messagesReceived;
+ private int messagesStored;
+ private int mailSent;
+ private int smsSent;
+ private int geocoderRequests;
+ private int geolocationRequests;
+
+ @Inject
+ public StatisticsManager(Config config, DataManager dataManager, Client client) {
+ this.config = config;
+ this.dataManager = dataManager;
+ this.client = client;
+ }
+
+ private void checkSplit() {
+ int currentUpdate = Calendar.getInstance().get(SPLIT_MODE);
+ if (lastUpdate.getAndSet(currentUpdate) != currentUpdate) {
+ Statistics statistics = new Statistics();
+ statistics.setCaptureTime(new Date());
+ statistics.setActiveUsers(users.size());
+ statistics.setActiveDevices(devices.size());
+ statistics.setRequests(requests);
+ statistics.setMessagesReceived(messagesReceived);
+ statistics.setMessagesStored(messagesStored);
+ statistics.setMailSent(mailSent);
+ statistics.setSmsSent(smsSent);
+ statistics.setGeocoderRequests(geocoderRequests);
+ statistics.setGeolocationRequests(geolocationRequests);
+
+ try {
+ dataManager.addObject(statistics);
+ } catch (SQLException e) {
+ LOGGER.warn("Error saving statistics", e);
+ }
+
+ String url = config.getString(Keys.SERVER_STATISTICS);
+ if (url != null) {
+ String time = DateUtil.formatDate(statistics.getCaptureTime());
+
+ Form form = new Form();
+ form.param("version", getClass().getPackage().getImplementationVersion());
+ form.param("captureTime", time);
+ form.param("activeUsers", String.valueOf(statistics.getActiveUsers()));
+ form.param("activeDevices", String.valueOf(statistics.getActiveDevices()));
+ form.param("requests", String.valueOf(statistics.getRequests()));
+ form.param("messagesReceived", String.valueOf(statistics.getMessagesReceived()));
+ form.param("messagesStored", String.valueOf(statistics.getMessagesStored()));
+ form.param("mailSent", String.valueOf(statistics.getMailSent()));
+ form.param("smsSent", String.valueOf(statistics.getSmsSent()));
+ form.param("geocoderRequests", String.valueOf(statistics.getGeocoderRequests()));
+ form.param("geolocationRequests", String.valueOf(statistics.getGeolocationRequests()));
+
+ client.target(url).request().async().post(Entity.form(form));
+ }
+
+ users.clear();
+ devices.clear();
+ requests = 0;
+ messagesReceived = 0;
+ messagesStored = 0;
+ mailSent = 0;
+ smsSent = 0;
+ geocoderRequests = 0;
+ geolocationRequests = 0;
+ }
+ }
+
+ public synchronized void registerRequest(long userId) {
+ checkSplit();
+ requests += 1;
+ if (userId != 0) {
+ users.add(userId);
+ }
+ }
+
+ public synchronized void registerMessageReceived() {
+ checkSplit();
+ messagesReceived += 1;
+ }
+
+ public synchronized void registerMessageStored(long deviceId) {
+ checkSplit();
+ messagesStored += 1;
+ if (deviceId != 0) {
+ devices.add(deviceId);
+ }
+ }
+
+ public synchronized void registerMail() {
+ checkSplit();
+ mailSent += 1;
+ }
+
+ public synchronized void registerSms() {
+ checkSplit();
+ smsSent += 1;
+ }
+
+ public synchronized void registerGeocoderRequest() {
+ checkSplit();
+ geocoderRequests += 1;
+ }
+
+ public synchronized void registerGeolocationRequest() {
+ checkSplit();
+ geolocationRequests += 1;
+ }
+
+}
diff --git a/src/main/java/org/traccar/database/UsersManager.java b/src/main/java/org/traccar/database/UsersManager.java
new file mode 100644
index 000000000..576a9e6c7
--- /dev/null
+++ b/src/main/java/org/traccar/database/UsersManager.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 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.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.traccar.model.User;
+
+public class UsersManager extends SimpleObjectManager<User> {
+
+ private Map<String, User> usersTokens;
+
+ public UsersManager(DataManager dataManager) {
+ super(dataManager, User.class);
+ if (usersTokens == null) {
+ usersTokens = new ConcurrentHashMap<>();
+ }
+ }
+
+ private void putToken(User user) {
+ if (usersTokens == null) {
+ usersTokens = new ConcurrentHashMap<>();
+ }
+ if (user.getToken() != null) {
+ usersTokens.put(user.getToken(), user);
+ }
+ }
+
+ @Override
+ protected void addNewItem(User user) {
+ super.addNewItem(user);
+ putToken(user);
+ }
+
+ @Override
+ protected void updateCachedItem(User user) {
+ User cachedUser = getById(user.getId());
+ super.updateCachedItem(user);
+ putToken(user);
+ if (cachedUser.getToken() != null && !cachedUser.getToken().equals(user.getToken())) {
+ usersTokens.remove(cachedUser.getToken());
+ }
+ }
+
+ @Override
+ protected void removeCachedItem(long userId) {
+ User cachedUser = getById(userId);
+ if (cachedUser != null) {
+ String userToken = cachedUser.getToken();
+ super.removeCachedItem(userId);
+ if (userToken != null) {
+ usersTokens.remove(userToken);
+ }
+ }
+ }
+
+ @Override
+ public Set<Long> getManagedItems(long userId) {
+ Set<Long> result = new HashSet<>();
+ result.addAll(getUserItems(userId));
+ result.add(userId);
+ return result;
+ }
+
+ public User getUserByToken(String token) {
+ return usersTokens.get(token);
+ }
+
+}