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    }