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 }