001    package org.deepamehta.plugins.subscriptions;
002    
003    import de.deepamehta.core.Association;
004    import de.deepamehta.core.DeepaMehtaObject;
005    import de.deepamehta.core.RelatedTopic;
006    import de.deepamehta.core.Topic;
007    import de.deepamehta.core.model.*;
008    import de.deepamehta.core.osgi.PluginActivator;
009    import de.deepamehta.core.service.Inject;
010    import de.deepamehta.core.service.ResultList;
011    import de.deepamehta.core.service.Transactional;
012    import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
013    import de.deepamehta.plugins.websockets.event.WebsocketTextMessageListener;
014    import de.deepamehta.plugins.websockets.service.WebSocketsService;
015    import java.util.ArrayList;
016    import java.util.Iterator;
017    import java.util.List;
018    import java.util.logging.Logger;
019    import javax.ws.rs.*;
020    import javax.ws.rs.core.Response;
021    import org.deepamehta.plugins.subscriptions.service.SubscriptionService;
022    
023    /**
024     *
025     * A DeepaMehta 4 Plugin introducing notifications on subscribed topics based on dm4-websockets.
026     *
027     * @author Malte Reißig (<malte@mikromedia.de>)
028     * @website https://github.com/mukil/dm4-subscriptions
029     * @version 1.0.3
030     *
031     */
032    
033    @Path("/subscriptions")
034    public class SubscriptionsPlugin extends PluginActivator implements SubscriptionService,
035                                                                        WebsocketTextMessageListener {
036    
037        private static Logger log = Logger.getLogger(SubscriptionsPlugin.class.getName());
038    
039        private static final String NOTIFICATION_TYPE = "org.deepamehta.subscriptions.notification";
040        private static final String NOTIFICATION_TITLE_TYPE = "org.deepamehta.subscriptions.notification_title";
041        private static final String NOTIFICATION_BODY_TYPE = "org.deepamehta.subscriptions.notification_body";
042        private static final String NOTIFICATION_INVOLVED_ITEM_ID_TYPE = "org.deepamehta.subscriptions.involved_item_id";
043        private static final String NOTIFICATION_SUB_ITEM_ID_TYPE = "org.deepamehta.subscriptions.subscribed_item_id";
044        private static final String NOTIFICATION_RECIPIENT_EDGE_TYPE =
045                "org.deepamehta.subscriptions.notification_recipient_edge";
046        private static final String SUBSCRIPTION_EDGE_TYPE = "org.deepamehta.subscriptions.subscription_edge";
047        private static final String NOTIFICATION_SEEN_TYPE = "org.deepamehta.subscriptions.notification_seen";
048    
049        // These two types of information can currently be subscribed (with their special semantics)
050        private static final String USER_ACCOUNT_TYPE = "dm4.accesscontrol.user_account";
051        private static final String DEEPAMEHTA_TAG_TYPE = "dm4.tags.tag";
052    
053        private static final String DEFAULT_ROLE_TYPE = "dm4.core.default";
054    
055        @Inject
056        private AccessControlService aclService = null;
057        @Inject
058        private WebSocketsService webSocketsService = null;
059    
060        
061    
062        @GET
063        @Path("/subscribe/{itemId}")
064        @Transactional
065        public Response subscribeUser(@PathParam("itemId") long itemId) {
066            // 0) Check for any session
067            String logged_in_username = aclService.getUsername();
068            if (!logged_in_username.isEmpty()) {
069                Topic account = getUserAccountTopic(logged_in_username);
070                // 1) Users can just manage their own subscriptions
071                subscribe(account.getId(), itemId);
072                return Response.ok().build();
073            }
074            return Response.noContent().build();
075        }
076    
077        @GET
078        @Path("/unsubscribe/{itemId}")
079        @Transactional
080        public Response unsubscribeUser(@PathParam("itemId") long itemId) {
081            // 0) Check for any session
082            String logged_in_username = aclService.getUsername();
083            if (!logged_in_username.isEmpty()) {
084                Topic account = getUserAccountTopic(logged_in_username);
085                // 1) Users can just manage their own subscriptions
086                unsubscribe(account.getId(), itemId);
087                return Response.ok().build();
088            }
089            return Response.noContent().build();
090        }
091    
092        @GET
093        @Path("/list")
094        public ResultList<RelatedTopic> getSubscriptions() {
095            // 0) Check for any session
096            String logged_in_username = aclService.getUsername();
097            if (logged_in_username == null || logged_in_username.isEmpty()) return null;
098            Topic account = getUserAccountTopic(logged_in_username);
099            // 1) Return results
100            log.fine("Listing all subscriptions of user " + account.getSimpleValue());
101            return account.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE, 0);
102        }
103    
104        @GET
105        @Path("/notification/seen/{newsId}")
106        @Transactional
107        public boolean setNotificationSeen(@PathParam("newsId") long newsId) {
108            try {
109                // 0) Check for any session
110                String logged_in_username = aclService.getUsername();
111                if (logged_in_username == null || logged_in_username.isEmpty()) {
112                    log.warning("Nobody logged in for whom we could set the notification as seen.");
113                }
114                Topic notification = dms.getTopic(newsId).loadChildTopics();
115                notification.getChildTopics().set(NOTIFICATION_SEEN_TYPE, true);
116                // 1) Do operation
117                log.fine("Set notification " + newsId + " as seen!");
118                return true;
119            } catch (Exception e) {
120                log.warning("Could NOT set notification " + newsId + " as seen! Caused by: " 
121                        + e.getCause().toString() + ", " + e.getMessage());
122                return false;
123            }
124        }
125    
126        @GET
127        @Path("/notifications/all")
128        public ResultList<RelatedTopic> getAllNotificationsForUser() {
129            return getAllNotifications();
130        }
131    
132        @GET
133        @Path("/notifications/unseen")
134        public ArrayList<RelatedTopic> getAllUnseenNotificationsForUser() {
135            return getAllUnseenNotifications();
136        }
137    
138        @Override
139        @Transactional
140        public void subscribe(long accountId, long itemId) {
141            try {
142                // 1) Check sanity of subscription
143                Topic itemToSubscribe = dms.getTopic(itemId);
144                if (!itemToSubscribe.getTypeUri().equals(DEEPAMEHTA_TAG_TYPE)
145                        && !itemToSubscribe.getTypeUri().equals(USER_ACCOUNT_TYPE)) {
146                    log.warning("Subscription are only supported for topics of type "
147                            + "\"User Account\" or \"Tag\" - Skipping creation of subscription");
148                }
149                // 2) Create subscriptions (if not alreay existent)
150                if (!associationExists(SUBSCRIPTION_EDGE_TYPE, itemId, accountId)) {
151                    AssociationModel model = new AssociationModel(SUBSCRIPTION_EDGE_TYPE,
152                        new TopicRoleModel(accountId, DEFAULT_ROLE_TYPE),
153                        new TopicRoleModel(itemId, DEFAULT_ROLE_TYPE),
154                        new ChildTopicsModel().addRef("org.deepamehta.subscriptions.subscription_type",
155                        "org.deepamehta.subscriptions.in_app_subscription"));
156                    dms.createAssociation(model);
157                    log.info("New subscription for user:" + accountId + " to item:" + itemId);
158                } else {
159                    log.info("Subscription already exists between " + accountId + " and " + itemId);
160                }
161            } catch (Exception e) {
162                log.warning("ROLLBACK!");
163                log.warning("Subscription between " +accountId+ " and " +itemId+ " not created.");
164                throw new RuntimeException(e);
165            }
166        }
167    
168        @Override
169        @Transactional
170        public void unsubscribe(long accountId, long itemId) {
171            List<Association> assocs = dms.getAssociations(accountId, itemId, SUBSCRIPTION_EDGE_TYPE);
172            Iterator<Association> iterator = assocs.iterator();
173            while (iterator.hasNext()) {
174                Association assoc = iterator.next();
175                dms.deleteAssociation(assoc.getId());
176            }
177        }
178    
179        @Override
180        @Transactional
181        public void createNotifications(String title, String message, long actionAccountId, DeepaMehtaObject item) {
182            if (item.getTypeUri().equals(USER_ACCOUNT_TYPE)) {
183                // 1) create notifications for all direct subscribers of this user topic
184                log.fine("Notifying subscribers of user account \"" + item.getSimpleValue() + "\"");
185                createNotifications(title, "", actionAccountId, item);
186            } else {
187                // 2) create notifications for all subscribers of all tags this topic is tagged with
188                if (item.getModel().getChildTopicsModel().has(DEEPAMEHTA_TAG_TYPE)) {
189                    // 2.1) go trough all tags of this topic
190                    List<TopicModel> tags = item.getModel().getChildTopicsModel().getTopics(DEEPAMEHTA_TAG_TYPE);
191                    for (TopicModel tag : tags) {
192                        Topic tag_node = dms.getTopic(tag.getId()).loadChildTopics();
193                        log.fine("Notifying subscribers of tag \"" + tag_node.getSimpleValue() + "\"");
194                        // for all subscribers of this tag
195                        createNotificationTopics(title, "", actionAccountId, item, tag_node);
196                    }
197                }
198                webSocketsService.broadcast("org.deepamehta.subscriptions", "Check notifications for your logged-in user.");
199            }
200        }
201    
202        @Override
203        public ResultList<RelatedTopic> getAllNotifications() {
204            String logged_in_username = aclService.getUsername();
205            if (logged_in_username == null || logged_in_username.isEmpty()) return null;
206            Topic account = getUserAccountTopic(logged_in_username);
207            //
208            ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE, 
209                "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0);
210            log.fine("Fetching " +results.getSize()+ " notifications for user " + account.getSimpleValue());
211            return results.loadChildTopics();
212        }
213    
214        @Override
215        public ArrayList<RelatedTopic> getAllUnseenNotifications() {
216            String logged_in_username = aclService.getUsername();
217            if (logged_in_username == null || logged_in_username.isEmpty()) return null;
218            Topic account = getUserAccountTopic(logged_in_username);
219            //
220            ArrayList<RelatedTopic> unseen = new ArrayList<RelatedTopic>();
221            ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE, 
222                "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0);
223            for (RelatedTopic notification : results.getItems()) {
224                boolean seen_child = notification.getChildTopics().getBoolean(NOTIFICATION_SEEN_TYPE);
225                if (!seen_child) {
226                    unseen.add(notification);
227                }
228            }
229            log.info("Fetching " +unseen.size() + " unseen notifications for user " + account.getSimpleValue());
230            return unseen;
231        }
232    
233        @Override
234        public void websocketTextMessage(String message) {
235            log.info("### Receiving message from WebSocket client: \"" + message + "\"");
236        }
237    
238        private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem) {
239            createNotificationTopics(title, text, accountId, involvedItem, null);
240        }
241    
242        private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem,
243                DeepaMehtaObject subscribedItem) {
244            // 0) Fetch all subscribers of item X
245            ResultList<RelatedTopic> subscribers = null;
246            long subscribedItemId = 0;
247            if (subscribedItem != null) { // fetch subscribers of subscribedItem
248                subscribers = subscribedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE,
249                    DEFAULT_ROLE_TYPE,  DEFAULT_ROLE_TYPE,  "dm4.accesscontrol.user_account", 0);
250                subscribedItemId = subscribedItem.getId();
251            } else { // fetch subscribers of involvedItem
252                subscribers = involvedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE,
253                    DEFAULT_ROLE_TYPE,  DEFAULT_ROLE_TYPE,  "dm4.accesscontrol.user_account",0);
254            }
255            for (RelatedTopic subscriber : subscribers) {
256                if (subscriber.getId() != accountId) {
257                    log.fine("> subscription is valid, notifying user " + subscriber.getSimpleValue());
258                    // 1) Create notification instance
259                    ChildTopicsModel message = new ChildTopicsModel()
260                            .put(NOTIFICATION_SEEN_TYPE, false)
261                            .put(NOTIFICATION_TITLE_TYPE, title)
262                            .put(NOTIFICATION_BODY_TYPE, text)
263                            .put(NOTIFICATION_SUB_ITEM_ID_TYPE, subscribedItemId)
264                            .putRef(USER_ACCOUNT_TYPE, accountId)
265                            .put(NOTIFICATION_INVOLVED_ITEM_ID_TYPE, involvedItem.getId());
266                    TopicModel model = new TopicModel(NOTIFICATION_TYPE, message);
267                    dms.createTopic(model); // check: is system the creator?
268                    // 2) Hook up notification with subscriber
269                    AssociationModel recipient_model = new AssociationModel(NOTIFICATION_RECIPIENT_EDGE_TYPE,
270                            model.createRoleModel(DEFAULT_ROLE_TYPE),
271                            new TopicRoleModel(subscriber.getId(), DEFAULT_ROLE_TYPE));
272                    dms.createAssociation(recipient_model); // check: is system the creator?
273                }
274            }
275        }
276    
277        private boolean associationExists(String edge_type, long itemId, long accountId) {
278            List<Association> results = dms.getAssociations(itemId, accountId, edge_type);
279            return (results.size() > 0) ? true : false;
280        }
281    
282        private Topic getUserAccountTopic(String username) {
283            Topic user = dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
284            return user.getRelatedTopic("dm4.core.composition", "dm4.core.child",
285                    "dm4.core.parent", "dm4.accesscontrol.user_account");
286        }
287    
288    }