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