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 }