001package systems.dmx.topicmaps;
002
003import systems.dmx.topicmaps.model.TopicmapViewmodel;
004
005import systems.dmx.core.Association;
006import systems.dmx.core.RelatedAssociation;
007import systems.dmx.core.RelatedTopic;
008import systems.dmx.core.Topic;
009import systems.dmx.core.model.AssociationModel;
010import systems.dmx.core.model.AssociationRoleModel;
011import systems.dmx.core.model.ChildTopicsModel;
012import systems.dmx.core.model.TopicModel;
013import systems.dmx.core.model.TopicRoleModel;
014import systems.dmx.core.model.topicmaps.AssociationViewModel;
015import systems.dmx.core.model.topicmaps.TopicViewModel;
016import systems.dmx.core.model.topicmaps.ViewProperties;
017import systems.dmx.core.osgi.PluginActivator;
018import systems.dmx.core.service.Transactional;
019import systems.dmx.core.util.DMXUtils;
020import systems.dmx.core.util.IdList;
021
022import org.codehaus.jettison.json.JSONObject;
023
024import javax.ws.rs.GET;
025import javax.ws.rs.PUT;
026import javax.ws.rs.POST;
027import javax.ws.rs.DELETE;
028import javax.ws.rs.Consumes;
029import javax.ws.rs.Path;
030import javax.ws.rs.PathParam;
031import javax.ws.rs.Produces;
032import javax.ws.rs.QueryParam;
033import javax.ws.rs.core.Context;
034
035import javax.servlet.http.HttpServletRequest;
036
037import java.io.InputStream;
038import java.util.ArrayList;
039import java.util.HashMap;
040import java.util.List;
041import java.util.Map;
042import java.util.concurrent.Callable;
043import java.util.logging.Level;
044import java.util.logging.Logger;
045
046
047
048@Path("/topicmap")
049@Consumes("application/json")
050@Produces("application/json")
051public class TopicmapsPlugin extends PluginActivator implements TopicmapsService {
052
053    // ------------------------------------------------------------------------------------------------------- Constants
054
055    // association type semantics ### TODO: to be dropped. Model-driven manipulators required.
056    private static final String TOPIC_MAPCONTEXT       = "dmx.topicmaps.topic_mapcontext";
057    private static final String ASSOCIATION_MAPCONTEXT = "dmx.topicmaps.association_mapcontext";
058    private static final String ROLE_TYPE_TOPICMAP     = "dmx.core.default";
059    private static final String ROLE_TYPE_TOPIC        = "dmx.topicmaps.topicmap_topic";
060    private static final String ROLE_TYPE_ASSOCIATION  = "dmx.topicmaps.topicmap_association";
061
062    private static final String PROP_X          = "dmx.topicmaps.x";
063    private static final String PROP_Y          = "dmx.topicmaps.y";
064    private static final String PROP_VISIBILITY = "dmx.topicmaps.visibility";
065    private static final String PROP_PINNED     = "dmx.topicmaps.pinned";
066
067    // ---------------------------------------------------------------------------------------------- Instance Variables
068
069    private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap();
070    private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList();
071    private Messenger me = new Messenger("systems.dmx.webclient");
072
073    @Context
074    private HttpServletRequest request;
075
076    private Logger logger = Logger.getLogger(getClass().getName());
077
078    // -------------------------------------------------------------------------------------------------- Public Methods
079
080
081
082    public TopicmapsPlugin() {
083        // Note: registering the default renderer in the init() hook would be too late.
084        // The renderer is already needed at install-in-DB time ### Still true? Use preInstall() hook?
085        registerTopicmapRenderer(new DefaultTopicmapRenderer());
086    }
087
088
089
090    // ***************************************
091    // *** TopicmapsService Implementation ***
092    // ***************************************
093
094
095
096    @POST
097    @Transactional
098    @Override
099    public Topic createTopicmap(@QueryParam("name") String name,
100                                @QueryParam("renderer_uri") String topicmapRendererUri,
101                                @QueryParam("private") boolean isPrivate) {
102        logger.info("Creating topicmap \"" + name + "\" (topicmapRendererUri=\"" + topicmapRendererUri +
103            "\", isPrivate=" + isPrivate +")");
104        Topic topicmapTopic = dmx.createTopic(mf.newTopicModel("dmx.topicmaps.topicmap", mf.newChildTopicsModel()
105            .put("dmx.topicmaps.name", name)
106            .put("dmx.topicmaps.topicmap_renderer_uri", topicmapRendererUri)
107            .put("dmx.topicmaps.private", isPrivate)
108            .put("dmx.topicmaps.state", getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(mf))));
109        me.newTopicmap(topicmapTopic);      // FIXME: broadcast to eligible users only
110        return topicmapTopic;
111    }
112
113    // ---
114
115    @GET
116    @Path("/{id}")
117    @Override
118    public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId,
119                                         @QueryParam("include_childs") boolean includeChilds) {
120        try {
121            logger.info("Loading topicmap " + topicmapId + " (includeChilds=" + includeChilds + ")");
122            // Note: a TopicmapViewmodel is not a DMXObject. So the JerseyResponseFilter's automatic
123            // child topic loading is not applied. We must load the child topics manually here.
124            Topic topicmapTopic = dmx.getTopic(topicmapId).loadChildTopics();
125            Map<Long, TopicViewModel> topics = fetchTopics(topicmapTopic, includeChilds);
126            Map<Long, AssociationViewModel> assocs = fetchAssociations(topicmapTopic);
127            //
128            return new TopicmapViewmodel(topicmapTopic.getModel(), topics, assocs);
129        } catch (Exception e) {
130            throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e);
131        }
132    }
133
134    @Override
135    public boolean isTopicInTopicmap(long topicmapId, long topicId) {
136        return fetchTopicMapcontext(topicmapId, topicId) != null;
137    }
138
139    @Override
140    public boolean isAssociationInTopicmap(long topicmapId, long assocId) {
141        return fetchAssociationMapcontext(topicmapId, assocId) != null;
142    }
143
144    // ---
145
146    @POST
147    @Path("/{id}/topic/{topic_id}")
148    @Transactional
149    @Override
150    public void addTopicToTopicmap(@PathParam("id") final long topicmapId,
151                                   @PathParam("topic_id") final long topicId, final ViewProperties viewProps) {
152        try {
153            // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable
154            dmx.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() {  // throws Exception
155                @Override
156                public Void call() {
157                    if (isTopicInTopicmap(topicmapId, topicId)) {
158                        throw new RuntimeException("Topic " + topicId + " already added to topicmap" + topicmapId);
159                    }
160                    createTopicMapcontext(topicmapId, topicId, viewProps);
161                    return null;
162                }
163            });
164        } catch (Exception e) {
165            throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " +
166                "(viewProps=" + viewProps + ")", e);
167        }
168    }
169
170    @Override
171    public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) {
172        addTopicToTopicmap(topicmapId, topicId, new ViewProperties(x, y, visibility, false));   // pinned=false
173    }
174
175    @POST
176    @Path("/{id}/association/{assoc_id}")
177    @Transactional
178    @Override
179    public void addAssociationToTopicmap(@PathParam("id") final long topicmapId,
180                                         @PathParam("assoc_id") final long assocId, final ViewProperties viewProps) {
181        try {
182            // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable
183            dmx.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() {  // throws Exception
184                @Override
185                public Void call() {
186                    if (isAssociationInTopicmap(topicmapId, assocId)) {
187                        throw new RuntimeException("Association " + assocId + " already added to topicmap " +
188                            topicmapId);
189                    }
190                    createAssociationMapcontext(topicmapId, assocId, viewProps);
191                    return null;
192                }
193            });
194        } catch (Exception e) {
195            throw new RuntimeException("Adding association " + assocId + " to topicmap " + topicmapId + " failed " +
196                "(viewProps=" + viewProps + ")", e);
197        }
198    }
199
200    @POST
201    @Path("/{id}/topic/{topic_id}/association/{assoc_id}")
202    @Transactional
203    @Override
204    public void addRelatedTopicToTopicmap(@PathParam("id") final long topicmapId,
205                                          @PathParam("topic_id") final long topicId,
206                                          @PathParam("assoc_id") final long assocId, final ViewProperties viewProps) {
207        try {
208            // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable
209            dmx.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() {  // throws Exception
210                @Override
211                public Void call() {
212                    // 1) add topic
213                    Association topicMapcontext = fetchTopicMapcontext(topicmapId, topicId);
214                    if (topicMapcontext == null) {
215                        createTopicMapcontext(topicmapId, topicId, viewProps);
216                    } else {
217                        if (!visibility(topicMapcontext)) {
218                            setTopicVisibility(topicmapId, topicId, true);
219                        }
220                    }
221                    // 2) add association
222                    // Note: it is an error if the association is already in the topicmap. In this case the topic is
223                    // already in the topicmap too, and the Webclient would not send the request in the first place.
224                    // ### TODO: rethink method contract. Do it analoguous to "add topic"?
225                    addAssociationToTopicmap(topicmapId, assocId, new ViewProperties().put(PROP_PINNED, false));
226                    return null;
227                }
228            });
229        } catch (Exception e) {
230            throw new RuntimeException("Adding related topic " + topicId + " (assocId=" + assocId + ") to topicmap " +
231                topicmapId + " failed (viewProps=" + viewProps + ")", e);
232        }
233    }
234
235    // ---
236
237    @PUT
238    @Path("/{id}/topic/{topic_id}")
239    @Transactional
240    @Override
241    public void setTopicViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
242                                                                         ViewProperties viewProps) {
243        storeTopicViewProperties(topicmapId, topicId, viewProps);
244    }
245
246    @PUT
247    @Path("/{id}/association/{assoc_id}")
248    @Transactional
249    @Override
250    public void setAssociationViewProperties(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId,
251                                                                               ViewProperties viewProps) {
252        storeAssociationViewProperties(topicmapId, assocId, viewProps);
253    }
254
255    @PUT
256    @Path("/{id}/topic/{topic_id}/{x}/{y}")
257    @Transactional
258    @Override
259    public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
260                                                                   @PathParam("x") int x, @PathParam("y") int y) {
261        try {
262            storeTopicViewProperties(topicmapId, topicId, new ViewProperties(x, y));
263            me.setTopicPosition(topicmapId, topicId, x, y);
264        } catch (Exception e) {
265            throw new RuntimeException("Setting position of topic " + topicId + " in topicmap " + topicmapId +
266                " failed ", e);
267        }
268    }
269
270    @PUT
271    @Path("/{id}/topic/{topic_id}/{visibility}")
272    @Transactional
273    @Override
274    public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
275                                                                     @PathParam("visibility") boolean visibility) {
276        try {
277            // remove associations
278            if (!visibility) {
279                for (Association assoc : dmx.getTopic(topicId).getAssociations()) {
280                    Association assocMapcontext = fetchAssociationMapcontext(topicmapId, assoc.getId());
281                    if (assocMapcontext != null) {
282                        deleteAssociationMapcontext(assocMapcontext);
283                    }
284                }
285            }
286            // show/hide topic
287            storeTopicViewProperties(topicmapId, topicId, new ViewProperties(visibility));
288            // send message
289            me.setTopicVisibility(topicmapId, topicId, visibility);
290        } catch (Exception e) {
291            throw new RuntimeException("Setting visibility of topic " + topicId + " in topicmap " + topicmapId +
292                " failed ", e);
293        }
294    }
295
296    @DELETE
297    @Path("/{id}/association/{assoc_id}")
298    @Transactional
299    @Override
300    public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
301        try {
302            Association assocMapcontext = fetchAssociationMapcontext(topicmapId, assocId);
303            // Note: idempotence of remove-assoc-from-topicmap is needed for delete-muti
304            if (assocMapcontext != null) {
305                deleteAssociationMapcontext(assocMapcontext);
306                me.removeAssociationFromTopicmap(topicmapId, assocId);
307            }
308        } catch (Exception e) {
309            throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ",
310                e);
311        }
312    }
313
314    // ---
315
316    @PUT
317    @Path("/{id}/topics/{topicIds}/visibility/false")
318    @Transactional
319    @Override
320    public void hideTopics(@PathParam("id") long topicmapId, @PathParam("topicIds") IdList topicIds) {
321        hideMulti(topicmapId, topicIds, new IdList());
322    }
323
324    @PUT
325    @Path("/{id}/assocs/{assocIds}/visibility/false")
326    @Transactional
327    @Override
328    public void hideAssocs(@PathParam("id") long topicmapId, @PathParam("assocIds") IdList assocIds) {
329        hideMulti(topicmapId, new IdList(), assocIds);
330    }
331
332    @PUT
333    @Path("/{id}/topics/{topicIds}/assocs/{assocIds}/visibility/false")
334    @Transactional
335    @Override
336    public void hideMulti(@PathParam("id") long topicmapId, @PathParam("topicIds") IdList topicIds,
337                                                            @PathParam("assocIds") IdList assocIds) {
338        logger.info("topicmapId=" + topicmapId + ", topicIds=" + topicIds + ", assocIds=" + assocIds);
339        for (long id : topicIds) {
340            setTopicVisibility(topicmapId, id, false);
341        }
342        for (long id : assocIds) {
343            removeAssociationFromTopicmap(topicmapId, id);
344        }
345    }
346
347    // ---
348
349    @PUT
350    @Path("/{id}")
351    @Transactional
352    @Override
353    public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) {
354        for (ClusterCoords.Entry entry : coords) {
355            setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y);
356        }
357    }
358
359    @PUT
360    @Path("/{id}/translation/{x}/{y}")
361    @Transactional
362    @Override
363    public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX,
364                                                                         @PathParam("y") int transY) {
365        try {
366            ChildTopicsModel topicmapState = mf.newChildTopicsModel()
367                .put("dmx.topicmaps.state", mf.newChildTopicsModel()
368                    .put("dmx.topicmaps.translation", mf.newChildTopicsModel()
369                        .put("dmx.topicmaps.translation_x", transX)
370                        .put("dmx.topicmaps.translation_y", transY)));
371            dmx.updateTopic(mf.newTopicModel(topicmapId, topicmapState));
372        } catch (Exception e) {
373            throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
374                transX + ", transY=" + transY + ")", e);
375        }
376    }
377
378    // ---
379
380    @Override
381    public void registerTopicmapRenderer(TopicmapRenderer renderer) {
382        logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
383        topicmapRenderers.put(renderer.getUri(), renderer);
384    }
385
386    // ---
387
388    @Override
389    public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
390        logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
391        viewmodelCustomizers.add(customizer);
392    }
393
394    @Override
395    public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
396        logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
397        if (!viewmodelCustomizers.remove(customizer)) {
398            throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
399        }
400    }
401
402    // ---
403
404    // Note: not part of topicmaps service
405    @GET
406    @Path("/{id}")
407    @Produces("text/html")
408    public InputStream getTopicmapInWebclient() {
409        // Note: the path parameter is evaluated at client-side
410        return invokeWebclient();
411    }
412
413    // Note: not part of topicmaps service
414    @GET
415    @Path("/{id}/topic/{topic_id}")
416    @Produces("text/html")
417    public InputStream getTopicmapAndTopicInWebclient() {
418        // Note: the path parameters are evaluated at client-side
419        return invokeWebclient();
420    }
421
422
423
424    // ------------------------------------------------------------------------------------------------- Private Methods
425
426    // --- Fetch ---
427
428    private Map<Long, TopicViewModel> fetchTopics(Topic topicmapTopic, boolean includeChilds) {
429        Map<Long, TopicViewModel> topics = new HashMap();
430        List<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, "dmx.core.default",
431            "dmx.topicmaps.topicmap_topic", null);  // othersTopicTypeUri=null
432        if (includeChilds) {
433            DMXUtils.loadChildTopics(relTopics);
434        }
435        for (RelatedTopic topic : relTopics) {
436            topics.put(topic.getId(), createTopicViewModel(topic));
437        }
438        return topics;
439    }
440
441    private Map<Long, AssociationViewModel> fetchAssociations(Topic topicmapTopic) {
442        Map<Long, AssociationViewModel> assocs = new HashMap();
443        List<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT,
444            "dmx.core.default", "dmx.topicmaps.topicmap_association", null);
445        for (RelatedAssociation assoc : relAssocs) {
446            assocs.put(assoc.getId(), createAssocViewModel(assoc));
447        }
448        return assocs;
449    }
450
451    // ---
452
453    private TopicViewModel createTopicViewModel(RelatedTopic topic) {
454        try {
455            ViewProperties viewProps = fetchTopicViewProperties(topic.getRelatingAssociation());
456            invokeViewmodelCustomizers(topic, viewProps);
457            return mf.newTopicViewModel(topic.getModel(), viewProps);
458        } catch (Exception e) {
459            throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e);
460        }
461    }
462
463    private AssociationViewModel createAssocViewModel(RelatedAssociation assoc) {
464        try {
465            ViewProperties viewProps = fetchAssocViewProperties(assoc.getRelatingAssociation());
466            // invokeViewmodelCustomizers(assoc, viewProps);    // TODO: assoc customizers?
467            return mf.newAssociationViewModel(assoc.getModel(), viewProps);
468        } catch (Exception e) {
469            throw new RuntimeException("Creating viewmodel for association " + assoc.getId() + " failed", e);
470        }
471    }
472
473    // ---
474
475    private Association fetchTopicMapcontext(long topicmapId, long topicId) {
476        return dmx.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC);
477    }
478
479    private Association fetchAssociationMapcontext(long topicmapId, long assocId) {
480        return dmx.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
481            ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION);
482    }
483
484    // ---
485
486    private void createTopicMapcontext(long topicmapId, long topicId, ViewProperties viewProps) {
487        Association topicMapcontext = dmx.createAssociation(mf.newAssociationModel(TOPIC_MAPCONTEXT,
488            mf.newTopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP),
489            mf.newTopicRoleModel(topicId,    ROLE_TYPE_TOPIC)
490        ));
491        storeViewProperties(topicMapcontext, viewProps);
492        //
493        TopicViewModel topic = mf.newTopicViewModel(dmx.getTopic(topicId).getModel(), viewProps);
494        me.addTopicToTopicmap(topicmapId, topic);
495    }
496
497    private void createAssociationMapcontext(long topicmapId, long assocId, ViewProperties viewProps) {
498        Association assocMapcontext = dmx.createAssociation(mf.newAssociationModel(ASSOCIATION_MAPCONTEXT,
499            mf.newTopicRoleModel(topicmapId,    ROLE_TYPE_TOPICMAP),
500            mf.newAssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)
501        ));
502        storeViewProperties(assocMapcontext, viewProps);
503        //
504        AssociationModel assoc = dmx.getAssociation(assocId).getModel();    // TODO: view props
505        me.addAssociationToTopicmap(topicmapId, assoc);
506    }
507
508    // ---
509
510    private void deleteAssociationMapcontext(Association assocMapcontext) {
511        // Note: a mapcontext association has no workspace assignment -- it belongs to the system.
512        // Deleting a mapcontext association is a privileged operation.
513        dmx.getAccessControl().deleteAssociationMapcontext(assocMapcontext);
514    }
515
516    // ---
517
518    private ViewProperties fetchTopicViewProperties(Association topicMapcontext) {
519        return new ViewProperties(
520            (Integer) topicMapcontext.getProperty(PROP_X),
521            (Integer) topicMapcontext.getProperty(PROP_Y),
522            visibility(topicMapcontext),
523            pinned(topicMapcontext)
524        );
525    }
526
527    private ViewProperties fetchAssocViewProperties(Association assocMapcontext) {
528        return new ViewProperties().put(PROP_PINNED, pinned(assocMapcontext));
529    }
530
531    private boolean visibility(Association topicMapcontext) {
532        return (Boolean) topicMapcontext.getProperty(PROP_VISIBILITY);
533    }
534
535    private boolean pinned(Association mapcontext) {
536        return (Boolean) mapcontext.getProperty(PROP_PINNED);
537    }
538
539    // --- Store ---
540
541    /**
542     * Convenience.
543     */
544    private void storeTopicViewProperties(long topicmapId, long topicId, ViewProperties viewProps) {
545        try {
546            Association topicMapcontext = fetchTopicMapcontext(topicmapId, topicId);
547            if (topicMapcontext == null) {
548                throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId);
549            }
550            storeViewProperties(topicMapcontext, viewProps);
551        } catch (Exception e) {
552            throw new RuntimeException("Storing view properties of topic " + topicId + " failed " +
553                "(viewProps=" + viewProps + ")", e);
554        }
555    }
556
557    /**
558     * Convenience.
559     */
560    private void storeAssociationViewProperties(long topicmapId, long assocId, ViewProperties viewProps) {
561        try {
562            Association assocMapcontext = fetchAssociationMapcontext(topicmapId, assocId);
563            if (assocMapcontext == null) {
564                throw new RuntimeException("Association " + assocId + " is not contained in topicmap " + topicmapId);
565            }
566            storeViewProperties(assocMapcontext, viewProps);
567        } catch (Exception e) {
568            throw new RuntimeException("Storing view properties of association " + assocId + " failed " +
569                "(viewProps=" + viewProps + ")", e);
570        }
571    }
572
573    private void storeViewProperties(Association mapcontext, ViewProperties viewProps) {
574        for (String propUri : viewProps) {
575            mapcontext.setProperty(propUri, viewProps.get(propUri), false);    // addToIndex = false
576        }
577    }
578
579    // --- Viewmodel Customizers ---
580
581    private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) {
582        for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
583            invokeViewmodelCustomizer(customizer, topic, viewProps);
584        }
585    }
586
587    private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic,
588                                                                           ViewProperties viewProps) {
589        try {
590            customizer.enrichViewProperties(topic, viewProps);
591        } catch (Exception e) {
592            throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
593                "(customizer=\"" + customizer.getClass().getName() + "\")", e);
594        }
595    }
596
597    // --- Topicmap Renderers ---
598
599    private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
600        TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
601        //
602        if (renderer == null) {
603            throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
604        }
605        //
606        return renderer;
607    }
608
609    // ---
610
611    private InputStream invokeWebclient() {
612        return dmx.getPlugin("systems.dmx.webclient").getStaticResource("/web/index.html");
613    }
614
615    // ------------------------------------------------------------------------------------------------- Private Classes
616
617    private class Messenger {
618
619        private String pluginUri;
620
621        private Messenger(String pluginUri) {
622            this.pluginUri = pluginUri;
623        }
624
625        // ---
626
627        private void newTopicmap(Topic topicmapTopic) {
628            try {
629                messageToAllButOne(new JSONObject()
630                    .put("type", "newTopicmap")
631                    .put("args", new JSONObject()
632                        .put("topicmapTopic", topicmapTopic.toJSON())
633                    )
634                );
635            } catch (Exception e) {
636                logger.log(Level.WARNING, "Error while sending a \"newTopicmap\" message:", e);
637            }
638        }
639
640        private void addTopicToTopicmap(long topicmapId, TopicViewModel topic) {
641            try {
642                messageToAllButOne(new JSONObject()
643                    .put("type", "addTopicToTopicmap")
644                    .put("args", new JSONObject()
645                        .put("topicmapId", topicmapId)
646                        .put("viewTopic", topic.toJSON())
647                    )
648                );
649            } catch (Exception e) {
650                logger.log(Level.WARNING, "Error while sending a \"addTopicToTopicmap\" message:", e);
651            }
652        }
653
654        private void addAssociationToTopicmap(long topicmapId, AssociationModel assoc) {
655            try {
656                messageToAllButOne(new JSONObject()
657                    .put("type", "addAssocToTopicmap")
658                    .put("args", new JSONObject()
659                        .put("topicmapId", topicmapId)
660                        .put("assoc", assoc.toJSON())
661                    )
662                );
663            } catch (Exception e) {
664                logger.log(Level.WARNING, "Error while sending a \"addAssocToTopicmap\" message:", e);
665            }
666        }
667
668        private void setTopicPosition(long topicmapId, long topicId, int x, int y) {
669            try {
670                messageToAllButOne(new JSONObject()
671                    .put("type", "setTopicPosition")
672                    .put("args", new JSONObject()
673                        .put("topicmapId", topicmapId)
674                        .put("topicId", topicId)
675                        .put("pos", new JSONObject()
676                            .put("x", x)
677                            .put("y", y)
678                        )
679                    )
680                );
681            } catch (Exception e) {
682                logger.log(Level.WARNING, "Error while sending a \"setTopicPosition\" message:", e);
683            }
684        }
685
686        private void setTopicVisibility(long topicmapId, long topicId, boolean visibility) {
687            try {
688                messageToAllButOne(new JSONObject()
689                    .put("type", "setTopicVisibility")
690                    .put("args", new JSONObject()
691                        .put("topicmapId", topicmapId)
692                        .put("topicId", topicId)
693                        .put("visibility", visibility)
694                    )
695                );
696            } catch (Exception e) {
697                logger.log(Level.WARNING, "Error while sending a \"setTopicVisibility\" message:", e);
698            }
699        }
700
701        private void removeAssociationFromTopicmap(long topicmapId, long assocId) {
702            try {
703                messageToAllButOne(new JSONObject()
704                    .put("type", "removeAssocFromTopicmap")
705                    .put("args", new JSONObject()
706                        .put("topicmapId", topicmapId)
707                        .put("assocId", assocId)
708                    )
709                );
710            } catch (Exception e) {
711                logger.log(Level.WARNING, "Error while sending a \"removeAssocFromTopicmap\" message:", e);
712            }
713        }
714
715        // ---
716
717        private void messageToAllButOne(JSONObject message) {
718            dmx.getWebSocketsService().messageToAllButOne(request, pluginUri, message.toString());
719        }
720    }
721}