001    package de.deepamehta.plugins.topicmaps;
002    
003    import de.deepamehta.plugins.topicmaps.model.TopicmapViewmodel;
004    import de.deepamehta.plugins.topicmaps.model.TopicViewmodel;
005    import de.deepamehta.plugins.topicmaps.model.AssociationViewmodel;
006    import de.deepamehta.plugins.topicmaps.service.TopicmapsService;
007    
008    import de.deepamehta.core.Association;
009    import de.deepamehta.core.RelatedAssociation;
010    import de.deepamehta.core.RelatedTopic;
011    import de.deepamehta.core.Topic;
012    import de.deepamehta.core.model.AssociationModel;
013    import de.deepamehta.core.model.AssociationRoleModel;
014    import de.deepamehta.core.model.ChildTopicsModel;
015    import de.deepamehta.core.model.TopicModel;
016    import de.deepamehta.core.model.TopicRoleModel;
017    import de.deepamehta.core.osgi.PluginActivator;
018    import de.deepamehta.core.service.ResultList;
019    import de.deepamehta.core.service.Transactional;
020    
021    import javax.ws.rs.GET;
022    import javax.ws.rs.PUT;
023    import javax.ws.rs.POST;
024    import javax.ws.rs.DELETE;
025    import javax.ws.rs.Path;
026    import javax.ws.rs.PathParam;
027    import javax.ws.rs.QueryParam;
028    import javax.ws.rs.Produces;
029    import javax.ws.rs.Consumes;
030    
031    import java.io.InputStream;
032    import java.util.ArrayList;
033    import java.util.HashMap;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.logging.Logger;
037    
038    
039    
040    @Path("/topicmap")
041    @Consumes("application/json")
042    @Produces("application/json")
043    public class TopicmapsPlugin extends PluginActivator implements TopicmapsService {
044    
045        // ------------------------------------------------------------------------------------------------------- Constants
046    
047        private static final String DEFAULT_TOPICMAP_NAME     = "untitled";
048        private static final String DEFAULT_TOPICMAP_URI      = "dm4.topicmaps.default_topicmap";
049        private static final String DEFAULT_TOPICMAP_RENDERER = "dm4.webclient.default_topicmap_renderer";
050    
051        // association type semantics ### TODO: to be dropped. Model-driven manipulators required.
052        private static final String TOPIC_MAPCONTEXT       = "dm4.topicmaps.topic_mapcontext";
053        private static final String ASSOCIATION_MAPCONTEXT = "dm4.topicmaps.association_mapcontext";
054        private static final String ROLE_TYPE_TOPICMAP     = "dm4.core.default";
055        private static final String ROLE_TYPE_TOPIC        = "dm4.topicmaps.topicmap_topic";
056        private static final String ROLE_TYPE_ASSOCIATION  = "dm4.topicmaps.topicmap_association";
057    
058        // ---------------------------------------------------------------------------------------------- Instance Variables
059    
060        private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap();
061        private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList();
062    
063        private Logger logger = Logger.getLogger(getClass().getName());
064    
065        // -------------------------------------------------------------------------------------------------- Public Methods
066    
067    
068    
069        public TopicmapsPlugin() {
070            // Note: registering the default renderer in the InitializePluginListener would be too late.
071            // The renderer is already needed in the PostInstallPluginListener.
072            registerTopicmapRenderer(new DefaultTopicmapRenderer());
073        }
074    
075    
076    
077        // ***************************************
078        // *** TopicmapsService Implementation ***
079        // ***************************************
080    
081    
082    
083        @GET
084        @Path("/{id}")
085        @Override
086        public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId,
087                                             @QueryParam("include_childs") boolean includeChilds) {
088            try {
089                logger.info("Loading topicmap " + topicmapId + " (includeChilds=" + includeChilds + ")");
090                // Note: a TopicmapViewmodel is not a DeepaMehtaObject. So the JerseyResponseFilter's automatic
091                // child topic loading is not applied. We must load the child topics manually here.
092                Topic topicmapTopic = dms.getTopic(topicmapId).loadChildTopics();
093                Map<Long, TopicViewmodel> topics = fetchTopics(topicmapTopic, includeChilds);
094                Map<Long, AssociationViewmodel> assocs = fetchAssociations(topicmapTopic);
095                //
096                return new TopicmapViewmodel(topicmapTopic.getModel(), topics, assocs);
097            } catch (Exception e) {
098                throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e);
099            }
100        }
101    
102        // ---
103    
104        @POST
105        @Path("/{name}/{topicmap_renderer_uri}")
106        @Transactional
107        @Override
108        public Topic createTopicmap(@PathParam("name") String name,
109                                    @PathParam("topicmap_renderer_uri") String topicmapRendererUri) {
110            return createTopicmap(name, null, topicmapRendererUri);
111        }
112    
113        @Override
114        public Topic createTopicmap(String name, String uri, String topicmapRendererUri) {
115            ChildTopicsModel topicmapState = getTopicmapRenderer(topicmapRendererUri).initialTopicmapState();
116            return dms.createTopic(new TopicModel(uri, "dm4.topicmaps.topicmap", new ChildTopicsModel()
117                .put("dm4.topicmaps.name", name)
118                .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri)
119                .put("dm4.topicmaps.state", topicmapState)
120            ));
121        }
122    
123        // ---
124    
125        @POST
126        @Path("/{id}/topic/{topic_id}")
127        @Transactional
128        @Override
129        public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
130                                       ChildTopicsModel viewProps) {
131            try {
132                dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT,
133                    new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP),
134                    new TopicRoleModel(topicId,    ROLE_TYPE_TOPIC), viewProps
135                ));
136                storeCustomViewProperties(topicmapId, topicId, viewProps);
137            } catch (Exception e) {
138                throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " +
139                    "(viewProps=" + viewProps + ")", e);
140            }
141        }
142    
143        @Override
144        public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) {
145            addTopicToTopicmap(topicmapId, topicId, new StandardViewProperties(x, y, visibility));
146        }
147    
148        @POST
149        @Path("/{id}/association/{assoc_id}")
150        @Transactional
151        @Override
152        public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
153            dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT,
154                new TopicRoleModel(topicmapId,    ROLE_TYPE_TOPICMAP),
155                new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)
156            ));
157        }
158    
159        // ---
160    
161        @Override
162        public boolean isTopicInTopicmap(long topicmapId, long topicId) {
163            return fetchTopicRefAssociation(topicmapId, topicId) != null;
164        }
165    
166        // ---
167    
168        @PUT
169        @Path("/{id}/topic/{topic_id}")
170        @Transactional
171        @Override
172        public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
173                                                                        ChildTopicsModel viewProps) {
174            try {
175                storeStandardViewProperties(topicmapId, topicId, viewProps);
176                storeCustomViewProperties(topicmapId, topicId, viewProps);
177            } catch (Exception e) {
178                throw new RuntimeException("Storing view properties of topic " + topicId + " failed " +
179                    "(viewProps=" + viewProps + ")", e);
180            }
181        }
182    
183    
184        @PUT
185        @Path("/{id}/topic/{topic_id}/{x}/{y}")
186        @Transactional
187        @Override
188        public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
189                                                                       @PathParam("x") int x, @PathParam("y") int y) {
190            storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(x, y));
191        }
192    
193        @PUT
194        @Path("/{id}/topic/{topic_id}/{visibility}")
195        @Transactional
196        @Override
197        public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
198                                                                         @PathParam("visibility") boolean visibility) {
199            storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(visibility));
200        }
201    
202        @DELETE
203        @Path("/{id}/association/{assoc_id}")
204        @Transactional
205        @Override
206        public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
207            fetchAssociationRefAssociation(topicmapId, assocId).delete();
208        }
209    
210        // ---
211    
212        @PUT
213        @Path("/{id}")
214        @Transactional
215        @Override
216        public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) {
217            for (ClusterCoords.Entry entry : coords) {
218                setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y);
219            }
220        }
221    
222        @PUT
223        @Path("/{id}/translation/{x}/{y}")
224        @Transactional
225        @Override
226        public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX,
227                                                                             @PathParam("y") int transY) {
228            try {
229                ChildTopicsModel topicmapState = new ChildTopicsModel()
230                    .put("dm4.topicmaps.state", new ChildTopicsModel()
231                        .put("dm4.topicmaps.translation", new ChildTopicsModel()
232                            .put("dm4.topicmaps.translation_x", transX)
233                            .put("dm4.topicmaps.translation_y", transY)));
234                dms.updateTopic(new TopicModel(topicmapId, topicmapState));
235            } catch (Exception e) {
236                throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
237                    transX + ", transY=" + transY + ")", e);
238            }
239        }
240    
241        // ---
242    
243        @Override
244        public void registerTopicmapRenderer(TopicmapRenderer renderer) {
245            logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
246            topicmapRenderers.put(renderer.getUri(), renderer);
247        }
248    
249        // ---
250    
251        @Override
252        public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
253            logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
254            viewmodelCustomizers.add(customizer);
255        }
256    
257        @Override
258        public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
259            logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
260            if (!viewmodelCustomizers.remove(customizer)) {
261                throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
262            }
263        }
264    
265        // ---
266    
267        // Note: not part of topicmaps service
268        @GET
269        @Path("/{id}")
270        @Produces("text/html")
271        public InputStream getTopicmapInWebclient() {
272            // Note: the path parameter is evaluated at client-side
273            return invokeWebclient();
274        }
275    
276        // Note: not part of topicmaps service
277        @GET
278        @Path("/{id}/topic/{topic_id}")
279        @Produces("text/html")
280        public InputStream getTopicmapAndTopicInWebclient() {
281            // Note: the path parameters are evaluated at client-side
282            return invokeWebclient();
283        }
284    
285    
286    
287        // ****************************
288        // *** Hook Implementations ***
289        // ****************************
290    
291    
292    
293        @Override
294        public void postInstall() {
295            createTopicmap(DEFAULT_TOPICMAP_NAME, DEFAULT_TOPICMAP_URI, DEFAULT_TOPICMAP_RENDERER);
296            // Note: On post-install we have no workspace cookie.
297            // The workspace assignment is made by the Access Control plugin on all-plugins-active.
298        }
299    
300    
301    
302        // ------------------------------------------------------------------------------------------------- Private Methods
303    
304        // --- Fetch ---
305    
306        private Map<Long, TopicViewmodel> fetchTopics(Topic topicmapTopic, boolean includeChilds) {
307            Map<Long, TopicViewmodel> topics = new HashMap();
308            ResultList<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics("dm4.topicmaps.topic_mapcontext",
309                "dm4.core.default", "dm4.topicmaps.topicmap_topic", null, 0);   // othersTopicTypeUri=null, maxResultSize=0
310            if (includeChilds) {
311                relTopics.loadChildTopics();
312            }
313            for (RelatedTopic topic : relTopics) {
314                Association assoc = topic.getRelatingAssociation().loadChildTopics();
315                ChildTopicsModel viewProps = assoc.getChildTopics().getModel();
316                invokeViewmodelCustomizers("enrichViewProperties", topic, viewProps);
317                topics.put(topic.getId(), new TopicViewmodel(topic.getModel(), viewProps));
318            }
319            return topics;
320        }
321    
322        private Map<Long, AssociationViewmodel> fetchAssociations(Topic topicmapTopic) {
323            Map<Long, AssociationViewmodel> assocs = new HashMap();
324            ResultList<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(
325                "dm4.topicmaps.association_mapcontext", "dm4.core.default", "dm4.topicmaps.topicmap_association", null);
326            for (RelatedAssociation assoc : relAssocs) {
327                assocs.put(assoc.getId(), new AssociationViewmodel(assoc.getModel()));
328            }
329            return assocs;
330        }
331    
332        // ---
333    
334        private Association fetchTopicRefAssociation(long topicmapId, long topicId) {
335            return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC);
336        }
337    
338        private Association fetchAssociationRefAssociation(long topicmapId, long assocId) {
339            return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
340                ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION);
341        }
342    
343        // --- Store ---
344    
345        private void storeStandardViewProperties(long topicmapId, long topicId, ChildTopicsModel viewProps) {
346            fetchTopicRefAssociation(topicmapId, topicId).setChildTopics(viewProps);
347        }
348    
349        // ### Note: the topicmapId parameter is not used. Per-topicmap custom view properties not yet supported.
350        private void storeCustomViewProperties(long topicmapId, long topicId, ChildTopicsModel viewProps) {
351            invokeViewmodelCustomizers("storeViewProperties", dms.getTopic(topicId), viewProps);
352        }
353    
354        // --- Viewmodel Customizers ---
355    
356        private void invokeViewmodelCustomizers(String method, Topic topic, ChildTopicsModel viewProps) {
357            for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
358                invokeViewmodelCustomizer(customizer, method, topic, viewProps);
359            }
360        }
361    
362        private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, String method,
363                                                        Topic topic, ChildTopicsModel viewProps) {
364            try {
365                // we don't want use reflection here for performance reasons
366                if (method.equals("enrichViewProperties")) {
367                    customizer.enrichViewProperties(topic, viewProps);
368                } else if (method.equals("storeViewProperties")) {
369                    customizer.storeViewProperties(topic, viewProps);
370                } else {
371                    throw new RuntimeException("\"" + method + "\" is an unexpected method");
372                }
373            } catch (Exception e) {
374                throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
375                    "(customizer=\"" + customizer.getClass().getName() + "\", method=\"" + method + "\")", e);
376            }
377        }
378    
379        // --- Topicmap Renderers ---
380    
381        private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
382            TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
383            //
384            if (renderer == null) {
385                throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
386            }
387            //
388            return renderer;
389        }
390    
391        // ---
392    
393        private InputStream invokeWebclient() {
394            return dms.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html");
395        }
396    
397        // --------------------------------------------------------------------------------------------- Private Inner Class
398    
399        private class StandardViewProperties extends ChildTopicsModel {
400    
401            private StandardViewProperties(int x, int y, boolean visibility) {
402                put(x, y);
403                put(visibility);
404            }
405    
406            private StandardViewProperties(int x, int y) {
407                put(x, y);
408            }
409    
410    
411            private StandardViewProperties(boolean visibility) {
412                put(visibility);
413            }
414    
415            // ---
416    
417            private void put(int x, int y) {
418                put("dm4.topicmaps.x", x);
419                put("dm4.topicmaps.y", y);
420            }
421    
422            private void put(boolean visibility) {
423                put("dm4.topicmaps.visibility", visibility);
424            }
425        }
426    }