001    package de.deepamehta.plugins.topicmaps;
002    
003    import de.deepamehta.plugins.topicmaps.model.TopicmapViewmodel;
004    import de.deepamehta.plugins.topicmaps.service.TopicmapsService;
005    
006    import de.deepamehta.core.Association;
007    import de.deepamehta.core.Topic;
008    import de.deepamehta.core.model.AssociationModel;
009    import de.deepamehta.core.model.AssociationRoleModel;
010    import de.deepamehta.core.model.CompositeValueModel;
011    import de.deepamehta.core.model.TopicModel;
012    import de.deepamehta.core.model.TopicRoleModel;
013    import de.deepamehta.core.osgi.PluginActivator;
014    import de.deepamehta.core.service.ClientState;
015    import de.deepamehta.core.service.Directives;
016    import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
017    
018    import javax.ws.rs.GET;
019    import javax.ws.rs.PUT;
020    import javax.ws.rs.POST;
021    import javax.ws.rs.DELETE;
022    import javax.ws.rs.HeaderParam;
023    import javax.ws.rs.Path;
024    import javax.ws.rs.PathParam;
025    import javax.ws.rs.Produces;
026    import javax.ws.rs.Consumes;
027    
028    import java.io.InputStream;
029    import java.util.ArrayList;
030    import java.util.HashMap;
031    import java.util.List;
032    import java.util.Map;
033    import java.util.logging.Logger;
034    
035    
036    
037    @Path("/topicmap")
038    @Consumes("application/json")
039    @Produces("application/json")
040    public class TopicmapsPlugin extends PluginActivator implements TopicmapsService {
041    
042        // ------------------------------------------------------------------------------------------------------- Constants
043    
044        private static final String DEFAULT_TOPICMAP_NAME     = "untitled";
045        private static final String DEFAULT_TOPICMAP_URI      = "dm4.topicmaps.default_topicmap";
046        private static final String DEFAULT_TOPICMAP_RENDERER = "dm4.webclient.default_topicmap_renderer";
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        // ---------------------------------------------------------------------------------------------- Instance Variables
056    
057        private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap();
058        private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList();
059    
060        private Logger logger = Logger.getLogger(getClass().getName());
061    
062        // -------------------------------------------------------------------------------------------------- Public Methods
063    
064    
065    
066        public TopicmapsPlugin() {
067            // Note: registering the default renderer in the InitializePluginListener would be too late.
068            // The renderer is already needed in the PostInstallPluginListener.
069            registerTopicmapRenderer(new DefaultTopicmapRenderer());
070        }
071    
072    
073    
074        // ***************************************
075        // *** TopicmapsService Implementation ***
076        // ***************************************
077    
078    
079    
080        @GET
081        @Path("/{id}")
082        @Override
083        public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId) {
084            try {
085                return new TopicmapViewmodel(topicmapId, dms, viewmodelCustomizers);
086            } catch (Exception e) {
087                throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e);
088            }
089        }
090    
091        // ---
092    
093        @POST
094        @Path("/{name}/{topicmap_renderer_uri}")
095        @Override
096        public Topic createTopicmap(@PathParam("name") String name,
097                                    @PathParam("topicmap_renderer_uri") String topicmapRendererUri,
098                                    @HeaderParam("Cookie") ClientState clientState) {
099            return createTopicmap(name, null, topicmapRendererUri, clientState);
100        }
101    
102        @Override
103        public Topic createTopicmap(String name, String uri, String topicmapRendererUri, ClientState clientState) {
104            CompositeValueModel topicmapState = getTopicmapRenderer(topicmapRendererUri).initialTopicmapState();
105            return dms.createTopic(new TopicModel(uri, "dm4.topicmaps.topicmap", new CompositeValueModel()
106                .put("dm4.topicmaps.name", name)
107                .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri)
108                .put("dm4.topicmaps.state", topicmapState)), clientState);
109        }
110    
111        // ---
112    
113        @POST
114        @Path("/{id}/topic/{topic_id}")
115        @Override
116        public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
117                                       CompositeValueModel viewProps) {
118            DeepaMehtaTransaction tx = dms.beginTx();
119            try {
120                dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT,
121                    new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP),
122                    new TopicRoleModel(topicId,    ROLE_TYPE_TOPIC), viewProps), null); // FIXME: clientState=null
123                storeCustomViewProperties(topicmapId, topicId, viewProps);
124                //
125                tx.success();
126            } catch (Exception e) {
127                throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " +
128                    "(viewProps=" + viewProps + ")", e);
129            } finally {
130                tx.finish();
131            }
132        }
133    
134        @Override
135        public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) {
136            addTopicToTopicmap(topicmapId, topicId, new StandardViewProperties(x, y, visibility));
137        }
138    
139        @POST
140        @Path("/{id}/association/{assoc_id}")
141        @Override
142        public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
143            dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT,
144                new TopicRoleModel(topicmapId,    ROLE_TYPE_TOPICMAP),
145                new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)), null);       // FIXME: clientState=null
146        }
147    
148        // ---
149    
150        @Override
151        public boolean isTopicInTopicmap(long topicmapId, long topicId) {
152            return fetchTopicRefAssociation(topicmapId, topicId) != null;
153        }
154    
155        // ---
156    
157        @PUT
158        @Path("/{id}/topic/{topic_id}")
159        @Override
160        public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
161                                                                        CompositeValueModel viewProps) {
162            DeepaMehtaTransaction tx = dms.beginTx();
163            try {
164                storeStandardViewProperties(topicmapId, topicId, viewProps);
165                storeCustomViewProperties(topicmapId, topicId, viewProps);
166                //
167                tx.success();
168            } catch (Exception e) {
169                throw new RuntimeException("Storing view properties of topic " + topicId + " failed " +
170                    "(viewProps=" + viewProps + ")", e);
171            } finally {
172                tx.finish();
173            }
174        }
175    
176    
177        @PUT
178        @Path("/{id}/topic/{topic_id}/{x}/{y}")
179        @Override
180        public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
181                                                                       @PathParam("x") int x, @PathParam("y") int y) {
182            storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(x, y));
183        }
184    
185        @PUT
186        @Path("/{id}/topic/{topic_id}/{visibility}")
187        @Override
188        public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
189                                                                         @PathParam("visibility") boolean visibility) {
190            storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(visibility));
191        }
192    
193        @DELETE
194        @Path("/{id}/association/{assoc_id}")
195        @Override
196        public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
197            fetchAssociationRefAssociation(topicmapId, assocId).delete(new Directives());
198        }
199    
200        // ---
201    
202        @PUT
203        @Path("/{id}")
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        @Override
214        public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX,
215                                                                             @PathParam("y") int transY) {
216            try {
217                CompositeValueModel topicmapState = new CompositeValueModel()
218                    .put("dm4.topicmaps.state", new CompositeValueModel()
219                        .put("dm4.topicmaps.translation", new CompositeValueModel()
220                            .put("dm4.topicmaps.translation_x", transX)
221                            .put("dm4.topicmaps.translation_y", transY)));
222                dms.updateTopic(new TopicModel(topicmapId, topicmapState), null);
223            } catch (Exception e) {
224                throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
225                    transX + ", transY=" + transY + ")", e);
226            }
227        }
228    
229        // ---
230    
231        @Override
232        public void registerTopicmapRenderer(TopicmapRenderer renderer) {
233            logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
234            topicmapRenderers.put(renderer.getUri(), renderer);
235        }
236    
237        // ---
238    
239        @Override
240        public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
241            logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
242            viewmodelCustomizers.add(customizer);
243        }
244    
245        @Override
246        public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
247            logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
248            if (!viewmodelCustomizers.remove(customizer)) {
249                throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
250            }
251        }
252    
253        // ---
254    
255        // Note: not part of topicmaps service
256        @GET
257        @Path("/{id}")
258        @Produces("text/html")
259        public InputStream getTopicmapInWebclient() {
260            // Note: the path parameter is evaluated at client-side
261            return invokeWebclient();
262        }
263    
264        // Note: not part of topicmaps service
265        @GET
266        @Path("/{id}/topic/{topic_id}")
267        @Produces("text/html")
268        public InputStream getTopicmapAndTopicInWebclient() {
269            // Note: the path parameters are evaluated at client-side
270            return invokeWebclient();
271        }
272    
273    
274    
275        // ****************************
276        // *** Hook Implementations ***
277        // ****************************
278    
279    
280    
281        @Override
282        public void postInstall() {
283            createTopicmap(DEFAULT_TOPICMAP_NAME, DEFAULT_TOPICMAP_URI, DEFAULT_TOPICMAP_RENDERER, null);
284            // Note: null is passed as clientState. On post-install we have no clientState.
285            // The workspace assignment is made by the Access Control plugin on all-plugins-active.
286        }
287    
288    
289    
290        // ------------------------------------------------------------------------------------------------- Private Methods
291    
292        private void storeStandardViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) {
293            fetchTopicRefAssociation(topicmapId, topicId).setCompositeValue(viewProps, null, new Directives());
294        }                                                                           // clientState=null
295    
296        // ### Note: the topicmapId parameter is not used. Per-topicmap custom view properties not yet supported.
297        private void storeCustomViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) {
298            invokeViewmodelCustomizers(topicId, viewProps);
299        }
300    
301        // ---
302    
303        private Association fetchTopicRefAssociation(long topicmapId, long topicId) {
304            return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId,
305                ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC, false);        // fetchComposite=false
306        }
307    
308        private Association fetchAssociationRefAssociation(long topicmapId, long assocId) {
309            return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
310                ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION, false);  // fetchComposite=false
311        }
312    
313        // ---
314    
315        // ### There is a copy in TopicmapViewmodel
316        private void invokeViewmodelCustomizers(long topicId, CompositeValueModel viewProps) {
317            Topic topic = dms.getTopic(topicId, false);             // fetchComposite=false
318            for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
319                invokeViewmodelCustomizer(customizer, topic, viewProps);
320            }
321        }
322    
323        // ### There is a principal copy in TopicmapViewmodel
324        private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, Topic topic, CompositeValueModel viewProps) {
325            try {
326                customizer.storeViewProperties(topic, viewProps);
327            } catch (Exception e) {
328                throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
329                    "(customizer=\"" + customizer.getClass().getName() + "\", method=\"storeViewProperties\")", e);
330            }
331        }
332    
333        // ---
334    
335        private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
336            TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
337            //
338            if (renderer == null) {
339                throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
340            }
341            //
342            return renderer;
343        }
344    
345        // ---
346    
347        private InputStream invokeWebclient() {
348            try {
349                return dms.getPlugin("de.deepamehta.webclient").getResourceAsStream("web/index.html");
350            } catch (Exception e) {
351                throw new RuntimeException("Invoking the webclient failed", e);
352            }
353        }
354    
355        // --------------------------------------------------------------------------------------------- Private Inner Class
356    
357        private class StandardViewProperties extends CompositeValueModel {
358    
359            private StandardViewProperties(int x, int y, boolean visibility) {
360                put(x, y);
361                put(visibility);
362            }
363    
364            private StandardViewProperties(int x, int y) {
365                put(x, y);
366            }
367    
368    
369            private StandardViewProperties(boolean visibility) {
370                put(visibility);
371            }
372    
373            // ---
374    
375            private void put(int x, int y) {
376                put("dm4.topicmaps.x", x);
377                put("dm4.topicmaps.y", y);
378            }
379    
380            private void put(boolean visibility) {
381                put("dm4.topicmaps.visibility", visibility);
382            }
383        }
384    }