001 package com.poemspace.dm4;
002
003 import java.util.ArrayList;
004 import java.util.Arrays;
005 import java.util.Collections;
006 import java.util.Comparator;
007 import java.util.HashMap;
008 import java.util.HashSet;
009 import java.util.Iterator;
010 import java.util.List;
011 import java.util.Map;
012 import java.util.Set;
013 import java.util.logging.Logger;
014
015 import javax.ws.rs.GET;
016 import javax.ws.rs.HeaderParam;
017 import javax.ws.rs.POST;
018 import javax.ws.rs.PUT;
019 import javax.ws.rs.Path;
020 import javax.ws.rs.PathParam;
021 import javax.ws.rs.Produces;
022 import javax.ws.rs.core.MediaType;
023
024 import de.deepamehta.core.Association;
025 import de.deepamehta.core.DeepaMehtaObject;
026 import de.deepamehta.core.RelatedTopic;
027 import de.deepamehta.core.ResultSet;
028 import de.deepamehta.core.Topic;
029 import de.deepamehta.core.TopicType;
030 import de.deepamehta.core.ViewConfiguration;
031 import de.deepamehta.core.model.AssociationDefinitionModel;
032 import de.deepamehta.core.model.AssociationModel;
033 import de.deepamehta.core.model.IndexMode;
034 import de.deepamehta.core.model.TopicModel;
035 import de.deepamehta.core.model.TopicRoleModel;
036 import de.deepamehta.core.model.TopicTypeModel;
037 import de.deepamehta.core.osgi.PluginActivator;
038 import de.deepamehta.core.service.ClientState;
039 import de.deepamehta.core.service.Directives;
040 import de.deepamehta.core.service.PluginService;
041 import de.deepamehta.core.service.annotation.ConsumesService;
042 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
043 import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
044 import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
045 import de.deepamehta.plugins.accesscontrol.model.Operation;
046 import de.deepamehta.plugins.accesscontrol.model.UserRole;
047 import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
048 import de.deepamehta.plugins.mail.Mail;
049 import de.deepamehta.plugins.mail.StatusReport;
050 import de.deepamehta.plugins.mail.service.MailService;
051
052 @Path("/poemspace")
053 @Produces(MediaType.APPLICATION_JSON)
054 public class PoemSpacePlugin extends PluginActivator {
055
056 private static final String CAMPAIGN = "dm4.poemspace.campaign";
057
058 private static final String COUNT = "dm4.poemspace.campaign.count";
059
060 private static final String EXCLUDE = "dm4.poemspace.campaign.excl";
061
062 private static final String INCLUDE = "dm4.poemspace.campaign.adds";
063
064 private static Logger log = Logger.getLogger(PoemSpacePlugin.class.getName());
065
066 private AccessControlService acService;
067
068 private CriteriaCache criteria = null;
069
070 private MailService mailService;
071
072 private boolean isInitialized;
073
074 public static final Comparator<Topic> VALUE_COMPARATOR = new Comparator<Topic>() {
075 @Override
076 public int compare(Topic a, Topic b) {
077 return a.getSimpleValue().toString().compareTo(b.getSimpleValue().toString());
078 }
079 };
080
081 @GET
082 @Path("/criteria-types")
083 public List<Topic> getCriteriaTypes() {
084 return criteria.getTypes();
085 }
086
087 @POST
088 @Path("/criteria-reload")
089 public List<Topic> reloadCriteriaCache() {
090 criteria = new CriteriaCache(dms);
091 return getCriteriaTypes();
092 }
093
094 @POST
095 @Path("/criteria/{name}")
096 public Topic createCriteria(@PathParam("name") String name,//
097 @HeaderParam("Cookie") ClientState cookie) {
098 log.info("create criteria " + name);
099 // TODO sanitize name parameter
100 String uri = "dm4.poemspace.criteria." + name.trim().toLowerCase();
101
102 DeepaMehtaTransaction tx = dms.beginTx();
103 try {
104 TopicType type = dms.createTopicType(//
105 new TopicTypeModel(uri, name, "dm4.core.text"), cookie);
106 type.setIndexModes(new HashSet<IndexMode>(Arrays.asList(IndexMode.FULLTEXT)));
107
108 ViewConfiguration viewConfig = type.getViewConfig();
109 viewConfig.addSetting("dm4.webclient.view_config",//
110 "dm4.webclient.multi_renderer_uri", "dm4.webclient.checkbox_renderer");
111 viewConfig.addSetting("dm4.webclient.view_config",//
112 "dm4.webclient.show_in_create_menu", true);
113 viewConfig.addSetting("dm4.webclient.view_config",//
114 "dm4.webclient.searchable_as_unit", true);
115
116 // associate criteria type
117 dms.createAssociation(new AssociationModel("dm4.core.association",//
118 new TopicRoleModel("dm4.poemspace.criteria.type", "dm4.core.default"),//
119 new TopicRoleModel(type.getId(), "dm4.core.default"), null), cookie);
120
121 // create search type aggregates
122 for (Topic topic : mailService.getSearchParentTypes()) {
123 TopicType searchType = dms.getTopicType(topic.getUri());
124 searchType.addAssocDef(new AssociationDefinitionModel("dm4.core.aggregation_def",//
125 searchType.getUri(), type.getUri(), "dm4.core.one", "dm4.core.many"));
126 }
127
128 // renew cache
129 criteria = new CriteriaCache(dms);
130 tx.success();
131
132 return type;
133 } finally {
134 tx.finish();
135 }
136 }
137
138 @POST
139 @Path("/campaign/{id}/include/{recipient}")
140 public Association include(//
141 @PathParam("id") long campaignId,//
142 @PathParam("recipient") long recipientId,//
143 @HeaderParam("Cookie") ClientState cookie) {
144 log.info("include recipient " + recipientId + " into campaign " + campaignId);
145 return createOrUpdateRecipient(INCLUDE, campaignId, recipientId, cookie);
146 }
147
148 @POST
149 @Path("/campaign/{id}/exclude/{recipient}")
150 public Association exclude(//
151 @PathParam("id") long campaignId,//
152 @PathParam("recipient") long recipientId,//
153 @HeaderParam("Cookie") ClientState cookie) {
154 log.info("exclude recipient " + recipientId + " from campaign " + campaignId);
155 return createOrUpdateRecipient(EXCLUDE, campaignId, recipientId, cookie);
156 }
157
158 @GET
159 @Path("/campaign/{id}/recipients")
160 public List<Topic> queryCampaignRecipients(//
161 @PathParam("id") long campaignId,//
162 @HeaderParam("Cookie") ClientState cookie) {
163 log.info("get campaign " + campaignId + " recipients");
164 DeepaMehtaTransaction tx = dms.beginTx();
165 try {
166 Topic campaign = dms.getTopic(campaignId, true);
167
168 // get and sort recipients
169 List<Topic> recipients = queryCampaignRecipients(campaign);
170 Collections.sort(recipients, VALUE_COMPARATOR);
171
172 // update campaign count and return result
173 campaign.getCompositeValue().set(COUNT, recipients.size(), cookie, new Directives());
174 tx.success();
175 return recipients;
176 } catch (Exception e) {
177 throw new RuntimeException("recipients query of campaign " + campaignId + " failed", e);
178 } finally {
179 tx.finish();
180 }
181 }
182
183 /**
184 * Starts and returns a new campaign from a mail.
185 *
186 * @param mailId
187 * @param cookie
188 * @return Campaign associated with the starting mail.
189 */
190 @PUT
191 @Path("/mail/{id}/start")
192 public Topic startCampaign(//
193 @PathParam("id") long mailId,//
194 @HeaderParam("Cookie") ClientState cookie) {
195 log.info("start a campaign from mail " + mailId);
196 DeepaMehtaTransaction tx = dms.beginTx();
197 try {
198 Topic campaign = dms.createTopic(new TopicModel(CAMPAIGN), cookie);
199 dms.createAssociation(new AssociationModel("dm4.core.association",//
200 new TopicRoleModel(mailId, "dm4.core.default"),//
201 new TopicRoleModel(campaign.getId(), "dm4.core.default"), null), cookie);
202 tx.success();
203 return campaign;
204 } catch (Exception e) {
205 throw new RuntimeException("start a campaign from mail " + mailId + " failed", e);
206 } finally {
207 tx.finish();
208 }
209 }
210
211 /**
212 * Sends a campaign mail.
213 *
214 * @param mailId
215 * @param cookie
216 * @return Sent mail topic.
217 */
218 @PUT
219 @Path("/mail/{id}/send")
220 public StatusReport sendCampaignMail(//
221 @PathParam("id") long mailId,//
222 @HeaderParam("Cookie") ClientState cookie) {
223 log.info("send campaign mail " + mailId);
224 DeepaMehtaTransaction tx = dms.beginTx();
225 try {
226 Topic mail = dms.getTopic(mailId, false);
227 RelatedTopic campaign = mail.getRelatedTopic("dm4.core.association",//
228 "dm4.core.default", "dm4.core.default", CAMPAIGN, false, false);
229
230 // associate recipients of query result
231 mailService.associateValidatedRecipients(mailId, queryCampaignRecipients(campaign), cookie);
232
233 // send and report status
234 StatusReport report = mailService.send(new Mail(mailId, dms));
235 tx.success();
236 return report;
237 } catch (Exception e) {
238 throw new RuntimeException("send campaign mail " + mailId + " failed", e);
239 } finally {
240 tx.finish();
241 }
242 }
243
244 /**
245 * Initialize.
246 */
247 @Override
248 public void init() {
249 isInitialized = true;
250 configureIfReady();
251 }
252
253 @Override
254 @ConsumesService({ "de.deepamehta.plugins.accesscontrol.service.AccessControlService",
255 "de.deepamehta.plugins.mail.service.MailService" })
256 public void serviceArrived(PluginService service) {
257 if (service instanceof AccessControlService) {
258 acService = (AccessControlService) service;
259 } else if (service instanceof MailService) {
260 mailService = (MailService) service;
261 }
262 configureIfReady();
263 }
264
265 private void configureIfReady() {
266 if (isInitialized && acService != null && mailService != null) {
267 // TODO add update listener to reload cache (create, update, delete)
268 criteria = new CriteriaCache(dms);
269 checkACLsOfMigration();
270 }
271 }
272
273 @Override
274 public void serviceGone(PluginService service) {
275 if (service == acService) {
276 acService = null;
277 } else if (service == mailService) {
278 mailService = null;
279 }
280 }
281
282 private void checkACLsOfMigration() {
283 for (String typeUri : new String[] { "dm4.poemspace.project", //
284 "dm4.poemspace.year", //
285 "dm4.poemspace.affiliation", //
286 "dm4.poemspace.press", //
287 "dm4.poemspace.education", //
288 "dm4.poemspace.public", //
289 "dm4.poemspace.art", //
290 "dm4.poemspace.gattung" }) {
291 checkACLsOfTopics(typeUri);
292 }
293 }
294
295 private void checkACLsOfTopics(String typeUri) {
296 for (RelatedTopic topic : dms.getTopics(typeUri, false, 0)) {
297 checkACLsOfObject(topic);
298 }
299 }
300
301 private void checkACLsOfObject(DeepaMehtaObject instance) {
302 if (acService.getCreator(instance) == null) {
303 log.info("initial ACL update " + instance.getId() + ": " + instance.getSimpleValue());
304 Topic admin = acService.getUsername("admin");
305 String adminName = admin.getSimpleValue().toString();
306 acService.setCreator(instance, adminName);
307 acService.setOwner(instance, adminName);
308 acService.setACL(instance, new AccessControlList( //
309 new ACLEntry(Operation.WRITE, UserRole.OWNER)));
310 }
311 }
312
313 private List<Topic> queryCampaignRecipients(Topic campaign) {
314 List<Topic> recipients = new ArrayList<Topic>();
315 Set<String> searchTypeUris = getSearchTypeUris();
316 Map<String, Set<RelatedTopic>> criterionMap = getCriterionMap(campaign);
317
318 // get and add the first recipient list
319 Iterator<String> criteriaIterator = criterionMap.keySet().iterator();
320 if (criteriaIterator.hasNext()) {
321 String uri = criteriaIterator.next();
322 Set<RelatedTopic> topics = criterionMap.get(uri);
323 Set<Topic> and = getCriterionRecipients(topics, searchTypeUris);
324 recipients.addAll(and);
325 if (recipients.isEmpty() == false) { // merge each other list
326 while (criteriaIterator.hasNext()) {
327 uri = criteriaIterator.next();
328 topics = criterionMap.get(uri);
329 and = getCriterionRecipients(topics, searchTypeUris);
330 // TODO use iterator instead of cloned list
331 // TODO use map by ID to simplify contain check
332 for (Topic topic : new ArrayList<Topic>(recipients)) {
333 if (and.contains(topic) == false) {
334 recipients.remove(topic);
335 }
336 if (recipients.size() == 0) {
337 break;
338 }
339 }
340 }
341 }
342 }
343
344 // get and add includes
345 Iterator<RelatedTopic> includes = campaign.getRelatedTopics(INCLUDE, 0).iterator();
346 while (includes.hasNext()) {
347 RelatedTopic include = includes.next();
348 if (recipients.contains(include) == false) {
349 recipients.add(include);
350 }
351 }
352
353 // get and remove excludes
354 Iterator<RelatedTopic> excludes = campaign.getRelatedTopics(EXCLUDE, 0).iterator();
355 while (excludes.hasNext()) {
356 RelatedTopic exclude = excludes.next();
357 if (recipients.contains(exclude)) {
358 recipients.remove(exclude);
359 }
360 }
361 return recipients;
362 }
363
364 private Set<String> getSearchTypeUris() {
365 Set<String> uris = new HashSet<String>();
366 for (Topic topic : mailService.getSearchParentTypes()) {
367 uris.add(topic.getUri());
368 }
369 return uris;
370 }
371
372 /**
373 * Returns parent aggregates of each criterion.
374 *
375 * @param criterionList
376 * criterion topics
377 * @param searchTypeUris
378 * topic type URIs of possible recipients
379 * @return
380 */
381 private Set<Topic> getCriterionRecipients(Set<RelatedTopic> criterionList,//
382 Set<String> searchTypeUris) {
383 Set<Topic> recipients = new HashSet<Topic>();
384 for (Topic criterion : criterionList) {
385 for (RelatedTopic topic : dms.getTopic(criterion.getId(), false)//
386 .getRelatedTopics("dm4.core.aggregation", "dm4.core.child", "dm4.core.parent", //
387 null, false, false, 0)) {
388 if (searchTypeUris.contains(topic.getTypeUri())) {
389 recipients.add(topic);
390 }
391 }
392 }
393 return recipients;
394 }
395
396 /**
397 * Returns all criteria aggregations of a topic.
398 *
399 * @param topic
400 * @return criterion map of all aggregated criteria sub type instances
401 */
402 private Map<String, Set<RelatedTopic>> getCriterionMap(Topic topic) {
403 Map<String, Set<RelatedTopic>> criterionMap = new HashMap<String, Set<RelatedTopic>>();
404 for (String typeUri : criteria.getTypeUris()) {
405 ResultSet<RelatedTopic> relatedTopics = topic.getRelatedTopics("dm4.core.aggregation",//
406 "dm4.core.parent", "dm4.core.child", typeUri, false, false, 0);
407 if (relatedTopics.getSize() > 0) {
408 criterionMap.put(typeUri, relatedTopics.getItems());
409 }
410 }
411 return criterionMap;
412 }
413
414 private Association createOrUpdateRecipient(String typeUri, long campaignId, long recipientId,
415 ClientState clientState) {
416 log.fine("create recipient " + typeUri + " association");
417 Set<Association> associations = dms.getAssociations(campaignId, recipientId);
418 if (associations.size() > 1) {
419 throw new IllegalStateException("only one association is supported");
420 }
421 DeepaMehtaTransaction tx = dms.beginTx();
422 try {
423 for (Association association : associations) {
424 log.fine("update recipient " + typeUri + " association");
425 association.setTypeUri(typeUri);
426 return association; // only one association can be used
427 }
428 Association association = dms.createAssociation(new AssociationModel(typeUri,//
429 new TopicRoleModel(campaignId, "dm4.core.default"),//
430 new TopicRoleModel(recipientId, "dm4.core.default"), null), clientState);
431 tx.success();
432 return association;
433 } finally {
434 tx.finish();
435 }
436 }
437
438 }