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        @Override
125        public boolean isAssociationInTopicmap(long topicmapId, long assocId) {
126            return fetchAssociationRefAssociation(topicmapId, assocId) != null;
127        }
128    
129        // ---
130    
131        @POST
132        @Path("/{id}/topic/{topic_id}")
133        @Transactional
134        @Override
135        public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
136                                       ViewProperties viewProps) {
137            try {
138                if (isTopicInTopicmap(topicmapId, topicId)) {
139                    throw new RuntimeException("The topic is already added");
140                }
141                //
142                Association assoc = dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT,
143                    new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP),
144                    new TopicRoleModel(topicId,    ROLE_TYPE_TOPIC)
145                ));
146                storeViewProperties(assoc, viewProps);
147            } catch (Exception e) {
148                throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " +
149                    "(viewProps=" + viewProps + ")", e);
150            }
151        }
152    
153        @Override
154        public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) {
155            addTopicToTopicmap(topicmapId, topicId, new ViewProperties(x, y, visibility));
156        }
157    
158        @POST
159        @Path("/{id}/association/{assoc_id}")
160        @Transactional
161        @Override
162        public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
163            try {
164                if (isAssociationInTopicmap(topicmapId, assocId)) {
165                    throw new RuntimeException("The association is already added");
166                }
167                //
168                dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT,
169                    new TopicRoleModel(topicmapId,    ROLE_TYPE_TOPICMAP),
170                    new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)
171                ));
172            } catch (Exception e) {
173                throw new RuntimeException("Adding association " + assocId + " to topicmap " + topicmapId + " failed", e);
174            }
175        }
176    
177        // ---
178    
179        @PUT
180        @Path("/{id}/topic/{topic_id}")
181        @Transactional
182        @Override
183        public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
184                                                                        ViewProperties viewProps) {
185            storeViewProperties(topicmapId, topicId, viewProps);
186        }
187    
188    
189        @PUT
190        @Path("/{id}/topic/{topic_id}/{x}/{y}")
191        @Transactional
192        @Override
193        public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
194                                                                       @PathParam("x") int x, @PathParam("y") int y) {
195            storeViewProperties(topicmapId, topicId, new ViewProperties(x, y));
196        }
197    
198        @PUT
199        @Path("/{id}/topic/{topic_id}/{visibility}")
200        @Transactional
201        @Override
202        public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
203                                                                         @PathParam("visibility") boolean visibility) {
204            storeViewProperties(topicmapId, topicId, new ViewProperties(visibility));
205        }
206    
207        @DELETE
208        @Path("/{id}/association/{assoc_id}")
209        @Transactional
210        @Override
211        public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
212            try {
213                Association assoc = fetchAssociationRefAssociation(topicmapId, assocId);
214                if (assoc == null) {
215                    throw new RuntimeException("Association " + assocId + " is not contained in topicmap " + topicmapId);
216                }
217                assoc.delete();
218            } catch (Exception e) {
219                throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ",
220                    e);
221            }
222        }
223    
224        // ---
225    
226        @PUT
227        @Path("/{id}")
228        @Transactional
229        @Override
230        public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) {
231            for (ClusterCoords.Entry entry : coords) {
232                setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y);
233            }
234        }
235    
236        @PUT
237        @Path("/{id}/translation/{x}/{y}")
238        @Transactional
239        @Override
240        public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX,
241                                                                             @PathParam("y") int transY) {
242            try {
243                ChildTopicsModel topicmapState = new ChildTopicsModel()
244                    .put("dm4.topicmaps.state", new ChildTopicsModel()
245                        .put("dm4.topicmaps.translation", new ChildTopicsModel()
246                            .put("dm4.topicmaps.translation_x", transX)
247                            .put("dm4.topicmaps.translation_y", transY)));
248                dms.updateTopic(new TopicModel(topicmapId, topicmapState));
249            } catch (Exception e) {
250                throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
251                    transX + ", transY=" + transY + ")", e);
252            }
253        }
254    
255        // ---
256    
257        @Override
258        public void registerTopicmapRenderer(TopicmapRenderer renderer) {
259            logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
260            topicmapRenderers.put(renderer.getUri(), renderer);
261        }
262    
263        // ---
264    
265        @Override
266        public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
267            logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
268            viewmodelCustomizers.add(customizer);
269        }
270    
271        @Override
272        public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
273            logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
274            if (!viewmodelCustomizers.remove(customizer)) {
275                throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
276            }
277        }
278    
279        // ---
280    
281        // Note: not part of topicmaps service
282        @GET
283        @Path("/{id}")
284        @Produces("text/html")
285        public InputStream getTopicmapInWebclient() {
286            // Note: the path parameter is evaluated at client-side
287            return invokeWebclient();
288        }
289    
290        // Note: not part of topicmaps service
291        @GET
292        @Path("/{id}/topic/{topic_id}")
293        @Produces("text/html")
294        public InputStream getTopicmapAndTopicInWebclient() {
295            // Note: the path parameters are evaluated at client-side
296            return invokeWebclient();
297        }
298    
299    
300    
301        // ------------------------------------------------------------------------------------------------- Private Methods
302    
303        // --- Fetch ---
304    
305        private Map<Long, TopicViewmodel> fetchTopics(Topic topicmapTopic, boolean includeChilds) {
306            Map<Long, TopicViewmodel> topics = new HashMap();
307            ResultList<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT,
308                "dm4.core.default", "dm4.topicmaps.topicmap_topic", null, 0);   // othersTopicTypeUri=null, maxResultSize=0
309            if (includeChilds) {
310                relTopics.loadChildTopics();
311            }
312            for (RelatedTopic topic : relTopics) {
313                topics.put(topic.getId(), createTopicViewmodel(topic));
314            }
315            return topics;
316        }
317    
318        private Map<Long, AssociationViewmodel> fetchAssociations(Topic topicmapTopic) {
319            Map<Long, AssociationViewmodel> assocs = new HashMap();
320            ResultList<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT,
321                "dm4.core.default", "dm4.topicmaps.topicmap_association", null);
322            for (RelatedAssociation assoc : relAssocs) {
323                assocs.put(assoc.getId(), new AssociationViewmodel(assoc.getModel()));
324            }
325            return assocs;
326        }
327    
328        // ---
329    
330        private TopicViewmodel createTopicViewmodel(RelatedTopic topic) {
331            try {
332                ViewProperties viewProps = fetchViewProperties(topic.getRelatingAssociation());
333                invokeViewmodelCustomizers(topic, viewProps);
334                return new TopicViewmodel(topic.getModel(), viewProps);
335            } catch (Exception e) {
336                throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e);
337            }
338        }
339    
340        // ---
341    
342        private Association fetchTopicRefAssociation(long topicmapId, long topicId) {
343            return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC);
344        }
345    
346        private Association fetchAssociationRefAssociation(long topicmapId, long assocId) {
347            return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
348                ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION);
349        }
350    
351        // ---
352    
353        private ViewProperties fetchViewProperties(Association mapcontextAssoc) {
354            int x = (Integer) mapcontextAssoc.getProperty(PROP_X);
355            int y = (Integer) mapcontextAssoc.getProperty(PROP_Y);
356            boolean visibility = (Boolean) mapcontextAssoc.getProperty(PROP_VISIBILITY);
357            return new ViewProperties(x, y, visibility);
358        }
359    
360        // --- Store ---
361    
362        private void storeViewProperties(long topicmapId, long topicId, ViewProperties viewProps) {
363            try {
364                Association assoc = fetchTopicRefAssociation(topicmapId, topicId);
365                if (assoc == null) {
366                    throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId);
367                }
368                storeViewProperties(assoc, viewProps);
369            } catch (Exception e) {
370                throw new RuntimeException("Storing view properties of topic " + topicId + " failed " +
371                    "(viewProps=" + viewProps + ")", e);
372            }
373        }
374    
375        private void storeViewProperties(Association mapcontextAssoc, ViewProperties viewProps) {
376            for (String propUri : viewProps.propUris()) {
377                mapcontextAssoc.setProperty(propUri, viewProps.get(propUri), false);    // addToIndex = false
378            }
379        }
380    
381        // --- Viewmodel Customizers ---
382    
383        private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) {
384            for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
385                invokeViewmodelCustomizer(customizer, topic, viewProps);
386            }
387        }
388    
389        private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic,
390                                                                               ViewProperties viewProps) {
391            try {
392                customizer.enrichViewProperties(topic, viewProps);
393            } catch (Exception e) {
394                throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
395                    "(customizer=\"" + customizer.getClass().getName() + "\")", e);
396            }
397        }
398    
399        // --- Topicmap Renderers ---
400    
401        private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
402            TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
403            //
404            if (renderer == null) {
405                throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
406            }
407            //
408            return renderer;
409        }
410    
411        // ---
412    
413        private InputStream invokeWebclient() {
414            return dms.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html");
415        }
416    }