001package org.deepamehta.plugins.subscriptions;
002
003import de.deepamehta.core.Association;
004import de.deepamehta.core.DeepaMehtaObject;
005import de.deepamehta.core.RelatedTopic;
006import de.deepamehta.core.Topic;
007import de.deepamehta.core.model.*;
008import de.deepamehta.core.osgi.PluginActivator;
009import de.deepamehta.core.service.Inject;
010import de.deepamehta.core.service.ResultList;
011import de.deepamehta.core.service.Transactional;
012import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
013import de.deepamehta.plugins.websockets.event.WebsocketTextMessageListener;
014import de.deepamehta.plugins.websockets.service.WebSocketsService;
015import java.util.ArrayList;
016import java.util.Iterator;
017import java.util.List;
018import java.util.logging.Logger;
019import javax.ws.rs.*;
020import javax.ws.rs.core.Response;
021import 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")
034public 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}