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-SNAPSHOT
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 throw new RuntimeException("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)
143 Topic itemToSubscribe = dms.getTopic(itemId);
144 if (!itemToSubscribe.getTypeUri().equals(DEEPAMEHTA_TAG_TYPE)
145 && !itemToSubscribe.getTypeUri().equals(USER_ACCOUNT_TYPE)) {
146 throw new RuntimeException("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 throw new RuntimeException(e);
163 }
164 }
165
166 @Override
167 @Transactional
168 public void unsubscribe(long accountId, long itemId) {
169 List<Association> assocs = dms.getAssociations(accountId, itemId, SUBSCRIPTION_EDGE_TYPE);
170 Iterator<Association> iterator = assocs.iterator();
171 while (iterator.hasNext()) {
172 Association assoc = iterator.next();
173 dms.deleteAssociation(assoc.getId());
174 }
175 }
176
177 @Override
178 @Transactional
179 public void createNotifications(String title, String message, long actionAccountId, DeepaMehtaObject item) {
180 if (item.getTypeUri().equals(USER_ACCOUNT_TYPE)) {
181 // 1) create notifications for all direct subscribers of this user topic
182 log.fine("Notifying subscribers of user account \"" + item.getSimpleValue() + "\"");
183 createNotifications(title, "", actionAccountId, item);
184 } else {
185 // 2) create notifications for all subscribers of all tags this topic is tagged with
186 if (item.getModel().getChildTopicsModel().has(DEEPAMEHTA_TAG_TYPE)) {
187 // 2.1) go trough all tags of this topic
188 List<TopicModel> tags = item.getModel().getChildTopicsModel().getTopics(DEEPAMEHTA_TAG_TYPE);
189 for (TopicModel tag : tags) {
190 Topic tag_node = dms.getTopic(tag.getId()).loadChildTopics();
191 log.fine("Notifying subscribers of tag \"" + tag_node.getSimpleValue() + "\"");
192 // for all subscribers of this tag
193 createNotificationTopics(title, "", actionAccountId, item, tag_node);
194 }
195 }
196 webSocketsService.broadcast("org.deepamehta.subscriptions", "Check notifications for your logged-in user.");
197 }
198 }
199
200 @Override
201 public ResultList<RelatedTopic> getAllNotifications() {
202 String logged_in_username = aclService.getUsername();
203 if (logged_in_username == null || logged_in_username.isEmpty()) return null;
204 Topic account = getUserAccountTopic(logged_in_username);
205 //
206 ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE,
207 "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0);
208 log.fine("Fetching " +results.getSize()+ " notifications for user " + account.getSimpleValue());
209 return results.loadChildTopics();
210 }
211
212 @Override
213 public ArrayList<RelatedTopic> getAllUnseenNotifications() {
214 String logged_in_username = aclService.getUsername();
215 if (logged_in_username == null || logged_in_username.isEmpty()) return null;
216 Topic account = getUserAccountTopic(logged_in_username);
217 //
218 ArrayList<RelatedTopic> unseen = new ArrayList<RelatedTopic>();
219 ResultList<RelatedTopic> results = account.getRelatedTopics(NOTIFICATION_RECIPIENT_EDGE_TYPE,
220 "dm4.core.default", "dm4.core.default", NOTIFICATION_TYPE, 0);
221 for (RelatedTopic notification : results.getItems()) {
222 boolean seen_child = notification.getChildTopics().getBoolean(NOTIFICATION_SEEN_TYPE);
223 if (!seen_child) {
224 unseen.add(notification);
225 }
226 }
227 log.info("Fetching " +unseen.size() + " unseen notifications for user " + account.getSimpleValue());
228 return unseen;
229 }
230
231 @Override
232 public void websocketTextMessage(String message) {
233 log.info("### Receiving message from WebSocket client: \"" + message + "\"");
234 }
235
236 private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem) {
237 createNotificationTopics(title, text, accountId, involvedItem, null);
238 }
239
240 private void createNotificationTopics(String title, String text, long accountId, DeepaMehtaObject involvedItem,
241 DeepaMehtaObject subscribedItem) {
242 // 0) Fetch all subscribers of item X
243 ResultList<RelatedTopic> subscribers = null;
244 long subscribedItemId = 0;
245 if (subscribedItem != null) { // fetch subscribers of subscribedItem
246 subscribers = subscribedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE,
247 DEFAULT_ROLE_TYPE, DEFAULT_ROLE_TYPE, "dm4.accesscontrol.user_account", 0);
248 subscribedItemId = subscribedItem.getId();
249 } else { // fetch subscribers of involvedItem
250 subscribers = involvedItem.getRelatedTopics(SUBSCRIPTION_EDGE_TYPE,
251 DEFAULT_ROLE_TYPE, DEFAULT_ROLE_TYPE, "dm4.accesscontrol.user_account",0);
252 }
253 for (RelatedTopic subscriber : subscribers) {
254 if (subscriber.getId() != accountId) {
255 log.fine("> subscription is valid, notifying user " + subscriber.getSimpleValue());
256 // 1) Create notification instance
257 ChildTopicsModel message = new ChildTopicsModel()
258 .put(NOTIFICATION_SEEN_TYPE, false)
259 .put(NOTIFICATION_TITLE_TYPE, title)
260 .put(NOTIFICATION_BODY_TYPE, text)
261 .put(NOTIFICATION_SUB_ITEM_ID_TYPE, subscribedItemId)
262 .putRef(USER_ACCOUNT_TYPE, accountId)
263 .put(NOTIFICATION_INVOLVED_ITEM_ID_TYPE, involvedItem.getId());
264 TopicModel model = new TopicModel(NOTIFICATION_TYPE, message);
265 dms.createTopic(model); // check: is system the creator?
266 // 2) Hook up notification with subscriber
267 AssociationModel recipient_model = new AssociationModel(NOTIFICATION_RECIPIENT_EDGE_TYPE,
268 model.createRoleModel(DEFAULT_ROLE_TYPE),
269 new TopicRoleModel(subscriber.getId(), DEFAULT_ROLE_TYPE));
270 dms.createAssociation(recipient_model); // check: is system the creator?
271 }
272 }
273 }
274
275 private boolean associationExists(String edge_type, long itemId, long accountId) {
276 List<Association> results = dms.getAssociations(itemId, accountId, edge_type);
277 return (results.size() > 0) ? true : false;
278 }
279
280 private Topic getUserAccountTopic(String username) {
281 Topic user = dms.getTopic("dm4.accesscontrol.username", new SimpleValue(username));
282 return user.getRelatedTopic("dm4.core.composition", "dm4.core.child",
283 "dm4.core.parent", "dm4.accesscontrol.user_account");
284 }
285
286 }