/* * Copyright 2022 - 2023 Anton Tananaev (anton@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.traccar.session.cache; import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.traccar.broadcast.BroadcastInterface; import org.traccar.broadcast.BroadcastService; import org.traccar.config.Config; import org.traccar.model.Attribute; import org.traccar.model.BaseModel; import org.traccar.model.Calendar; import org.traccar.model.Device; import org.traccar.model.Driver; import org.traccar.model.Geofence; import org.traccar.model.Group; import org.traccar.model.GroupedModel; import org.traccar.model.Maintenance; import org.traccar.model.Notification; import org.traccar.model.ObjectOperation; import org.traccar.model.Permission; import org.traccar.model.Position; import org.traccar.model.Schedulable; import org.traccar.model.Server; import org.traccar.model.User; import org.traccar.storage.Storage; import org.traccar.storage.StorageException; import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @Singleton public class CacheManager implements BroadcastInterface { private static final Set> GROUPED_CLASSES = Set.of(Attribute.class, Driver.class, Geofence.class, Maintenance.class, Notification.class); private final Config config; private final Storage storage; private final BroadcastService broadcastService; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final CacheGraph graph = new CacheGraph(); private Server server; private final Map devicePositions = new HashMap<>(); private final Map deviceReferences = new HashMap<>(); @Inject public CacheManager(Config config, Storage storage, BroadcastService broadcastService) throws StorageException { this.config = config; this.storage = storage; this.broadcastService = broadcastService; server = storage.getObject(Server.class, new Request(new Columns.All())); broadcastService.registerListener(this); } @Override public String toString() { return graph.toString(); } public Config getConfig() { return config; } public T getObject(Class clazz, long id) { try { lock.readLock().lock(); return graph.getObject(clazz, id); } finally { lock.readLock().unlock(); } } public Set getDeviceObjects(long deviceId, Class clazz) { try { lock.readLock().lock(); return graph.getObjects(Device.class, deviceId, clazz, Set.of(Group.class), true) .collect(Collectors.toUnmodifiableSet()); } finally { lock.readLock().unlock(); } } public Position getPosition(long deviceId) { try { lock.readLock().lock(); return devicePositions.get(deviceId); } finally { lock.readLock().unlock(); } } public Server getServer() { try { lock.readLock().lock(); return server; } finally { lock.readLock().unlock(); } } public Set getNotificationUsers(long notificationId, long deviceId) { try { lock.readLock().lock(); Set deviceUsers = getDeviceObjects(deviceId, User.class); return graph.getObjects(Notification.class, notificationId, User.class, Set.of(), false) .filter(deviceUsers::contains) .collect(Collectors.toUnmodifiableSet()); } finally { lock.readLock().unlock(); } } public Set getDeviceNotifications(long deviceId) { try { lock.readLock().lock(); var direct = graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class), true) .map(BaseModel::getId) .collect(Collectors.toUnmodifiableSet()); return graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class, User.class), true) .filter(notification -> notification.getAlways() || direct.contains(notification.getId())) .collect(Collectors.toUnmodifiableSet()); } finally { lock.readLock().unlock(); } } public void addDevice(long deviceId) throws Exception { try { lock.writeLock().lock(); if (deviceReferences.computeIfAbsent(deviceId, k -> new AtomicInteger()).getAndIncrement() <= 0) { Device device = storage.getObject(Device.class, new Request( new Columns.All(), new Condition.Equals("id", deviceId))); graph.addObject(device); initializeCache(device); if (device.getPositionId() > 0) { devicePositions.put(deviceId, storage.getObject(Position.class, new Request( new Columns.All(), new Condition.Equals("id", device.getPositionId())))); } } } finally { lock.writeLock().unlock(); } } public void removeDevice(long deviceId) { try { lock.writeLock().lock(); if (deviceReferences.computeIfAbsent(deviceId, k -> new AtomicInteger()).decrementAndGet() <= 0) { graph.removeObject(Device.class, deviceId); devicePositions.remove(deviceId); deviceReferences.remove(deviceId); } } finally { lock.writeLock().unlock(); } } public void updatePosition(Position position) { try { lock.writeLock().lock(); if (deviceReferences.containsKey(position.getDeviceId())) { devicePositions.put(position.getDeviceId(), position); } } finally { lock.writeLock().unlock(); } } @Override public void invalidateObject( boolean local, Class clazz, long id, ObjectOperation operation) throws Exception { if (local) { broadcastService.invalidateObject(true, clazz, id, operation); } if (operation == ObjectOperation.DELETE) { graph.removeObject(clazz, id); } if (operation != ObjectOperation.UPDATE) { return; } if (clazz.equals(Server.class)) { server = storage.getObject(Server.class, new Request(new Columns.All())); return; } var after = storage.getObject(clazz, new Request(new Columns.All(), new Condition.Equals("id", id))); if (after == null) { return; } var before = getObject(after.getClass(), after.getId()); if (before == null) { return; } if (after instanceof GroupedModel) { long beforeGroupId = ((GroupedModel) before).getGroupId(); long afterGroupId = ((GroupedModel) after).getGroupId(); if (beforeGroupId != afterGroupId) { if (beforeGroupId > 0) { invalidatePermission(clazz, id, Group.class, beforeGroupId, false); } if (afterGroupId > 0) { invalidatePermission(clazz, id, Group.class, afterGroupId, true); } } } else if (after instanceof Schedulable) { long beforeCalendarId = ((Schedulable) before).getCalendarId(); long afterCalendarId = ((Schedulable) after).getCalendarId(); if (beforeCalendarId != afterCalendarId) { if (beforeCalendarId > 0) { invalidatePermission(clazz, id, Calendar.class, beforeCalendarId, false); } if (afterCalendarId > 0) { invalidatePermission(clazz, id, Calendar.class, afterCalendarId, true); } } // TODO handle notification always change } graph.updateObject(after); } @Override public void invalidatePermission( boolean local, Class clazz1, long id1, Class clazz2, long id2, boolean link) throws Exception { if (local) { broadcastService.invalidatePermission(true, clazz1, id1, clazz2, id2, link); } if (clazz1.equals(User.class) && GroupedModel.class.isAssignableFrom(clazz2)) { invalidatePermission(clazz2, id2, clazz1, id1, link); } else { invalidatePermission(clazz1, id1, clazz2, id2, link); } } private void invalidatePermission( Class fromClass, long fromId, Class toClass, long toId, boolean link) throws Exception { boolean groupLink = GroupedModel.class.isAssignableFrom(fromClass) && toClass.equals(Group.class); boolean calendarLink = Schedulable.class.isAssignableFrom(fromClass) && toClass.equals(Calendar.class); boolean userLink = fromClass.equals(User.class) && toClass.equals(Notification.class); boolean groupedLinks = GroupedModel.class.isAssignableFrom(fromClass) && (GROUPED_CLASSES.contains(toClass) || toClass.equals(User.class)); if (!groupLink && !calendarLink && !userLink && !groupedLinks) { return; } if (link) { BaseModel object = storage.getObject(toClass, new Request( new Columns.All(), new Condition.Equals("id", toId))); if (!graph.addLink(fromClass, fromId, object)) { initializeCache(object); } } else { graph.removeLink(fromClass, fromId, toClass, toId); } } private void initializeCache(BaseModel object) throws Exception { if (object instanceof User) { for (Permission permission : storage.getPermissions(User.class, Notification.class)) { if (permission.getOwnerId() == object.getId()) { invalidatePermission( permission.getOwnerClass(), permission.getOwnerId(), permission.getPropertyClass(), permission.getPropertyId(), true); } } } else { if (object instanceof GroupedModel groupedModel) { long groupId = groupedModel.getGroupId(); if (groupId > 0) { invalidatePermission(object.getClass(), object.getId(), Group.class, groupId, true); } for (Permission permission : storage.getPermissions(User.class, object.getClass())) { if (permission.getPropertyId() == object.getId()) { invalidatePermission( object.getClass(), object.getId(), User.class, permission.getOwnerId(), true); } } for (Class clazz : GROUPED_CLASSES) { for (Permission permission : storage.getPermissions(object.getClass(), clazz)) { if (permission.getOwnerId() == object.getId()) { invalidatePermission( object.getClass(), object.getId(), clazz, permission.getPropertyId(), true); } } } } if (object instanceof Schedulable schedulable) { long calendarId = schedulable.getCalendarId(); if (calendarId > 0) { invalidatePermission(object.getClass(), object.getId(), Calendar.class, calendarId, true); } } } } }