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