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