diff options
Diffstat (limited to 'src/main/java/org/traccar/database')
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); + } + +} |