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