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