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.concurrent.Callable;
037import java.util.logging.Logger;
038
039
040
041@Path("/topicmap")
042@Consumes("application/json")
043@Produces("application/json")
044public class TopicmapsPlugin extends PluginActivator implements TopicmapsService {
045
046    // ------------------------------------------------------------------------------------------------------- Constants
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    private static final String PROP_X          = "dm4.topicmaps.x";
056    private static final String PROP_Y          = "dm4.topicmaps.y";
057    private static final String PROP_VISIBILITY = "dm4.topicmaps.visibility";
058
059    // ---------------------------------------------------------------------------------------------- Instance Variables
060
061    private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap();
062    private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList();
063
064    private Logger logger = Logger.getLogger(getClass().getName());
065
066    // -------------------------------------------------------------------------------------------------- Public Methods
067
068
069
070    public TopicmapsPlugin() {
071        // Note: registering the default renderer in the init() hook would be too late.
072        // The renderer is already needed at install-in-DB time ### Still true?
073        registerTopicmapRenderer(new DefaultTopicmapRenderer());
074    }
075
076
077
078    // ***************************************
079    // *** TopicmapsService Implementation ***
080    // ***************************************
081
082
083
084    @POST
085    @Transactional
086    @Override
087    public Topic createTopicmap(@QueryParam("name") String name,
088                                @QueryParam("renderer_uri") String topicmapRendererUri,
089                                @QueryParam("private") boolean isPrivate) {
090        logger.info("Creating topicmap \"" + name + "\" (topicmapRendererUri=\"" + topicmapRendererUri +
091            "\", isPrivate=" + isPrivate +")");
092        return dm4.createTopic(mf.newTopicModel("dm4.topicmaps.topicmap", mf.newChildTopicsModel()
093            .put("dm4.topicmaps.name", name)
094            .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri)
095            .put("dm4.topicmaps.private", isPrivate)
096            .put("dm4.topicmaps.state", getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(mf))));
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") final long topicmapId,
137                                   @PathParam("topic_id") final long topicId, final ViewProperties viewProps) {
138        try {
139            // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable
140            dm4.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() {  // throws Exception
141                @Override
142                public Void call() {
143                    if (isTopicInTopicmap(topicmapId, topicId)) {
144                        throw new RuntimeException("The topic is already added");
145                    }
146                    //
147                    Association assoc = dm4.createAssociation(mf.newAssociationModel(TOPIC_MAPCONTEXT,
148                        mf.newTopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP),
149                        mf.newTopicRoleModel(topicId,    ROLE_TYPE_TOPIC)
150                    ));
151                    storeViewProperties(assoc, viewProps);
152                    return null;
153                }
154            });
155        } catch (Exception e) {
156            throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " +
157                "(viewProps=" + viewProps + ")", e);
158        }
159    }
160
161    @Override
162    public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) {
163        addTopicToTopicmap(topicmapId, topicId, new ViewProperties(x, y, visibility));
164    }
165
166    @POST
167    @Path("/{id}/association/{assoc_id}")
168    @Transactional
169    @Override
170    public void addAssociationToTopicmap(@PathParam("id") final long topicmapId,
171                                         @PathParam("assoc_id") final long assocId) {
172        try {
173            // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable
174            dm4.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() {  // throws Exception
175                @Override
176                public Void call() {
177                    if (isAssociationInTopicmap(topicmapId, assocId)) {
178                        throw new RuntimeException("The association is already added");
179                    }
180                    //
181                    dm4.createAssociation(mf.newAssociationModel(ASSOCIATION_MAPCONTEXT,
182                        mf.newTopicRoleModel(topicmapId,    ROLE_TYPE_TOPICMAP),
183                        mf.newAssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)
184                    ));
185                    return null;
186                }
187            });
188        } catch (Exception e) {
189            throw new RuntimeException("Adding association " + assocId + " to topicmap " + topicmapId + " failed", e);
190        }
191    }
192
193    // ---
194
195    @PUT
196    @Path("/{id}/topic/{topic_id}")
197    @Transactional
198    @Override
199    public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
200                                                                    ViewProperties viewProps) {
201        storeViewProperties(topicmapId, topicId, viewProps);
202    }
203
204
205    @PUT
206    @Path("/{id}/topic/{topic_id}/{x}/{y}")
207    @Transactional
208    @Override
209    public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
210                                                                   @PathParam("x") int x, @PathParam("y") int y) {
211        storeViewProperties(topicmapId, topicId, new ViewProperties(x, y));
212    }
213
214    @PUT
215    @Path("/{id}/topic/{topic_id}/{visibility}")
216    @Transactional
217    @Override
218    public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId,
219                                                                     @PathParam("visibility") boolean visibility) {
220        storeViewProperties(topicmapId, topicId, new ViewProperties(visibility));
221    }
222
223    @DELETE
224    @Path("/{id}/association/{assoc_id}")
225    @Transactional
226    @Override
227    public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) {
228        try {
229            Association assoc = fetchAssociationRefAssociation(topicmapId, assocId);
230            if (assoc == null) {
231                throw new RuntimeException("Association " + assocId + " is not contained in topicmap " + topicmapId);
232            }
233            assoc.delete();
234        } catch (Exception e) {
235            throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ",
236                e);
237        }
238    }
239
240    // ---
241
242    @PUT
243    @Path("/{id}")
244    @Transactional
245    @Override
246    public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) {
247        for (ClusterCoords.Entry entry : coords) {
248            setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y);
249        }
250    }
251
252    @PUT
253    @Path("/{id}/translation/{x}/{y}")
254    @Transactional
255    @Override
256    public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX,
257                                                                         @PathParam("y") int transY) {
258        try {
259            ChildTopicsModel topicmapState = mf.newChildTopicsModel()
260                .put("dm4.topicmaps.state", mf.newChildTopicsModel()
261                    .put("dm4.topicmaps.translation", mf.newChildTopicsModel()
262                        .put("dm4.topicmaps.translation_x", transX)
263                        .put("dm4.topicmaps.translation_y", transY)));
264            dm4.updateTopic(mf.newTopicModel(topicmapId, topicmapState));
265        } catch (Exception e) {
266            throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
267                transX + ", transY=" + transY + ")", e);
268        }
269    }
270
271    // ---
272
273    @Override
274    public void registerTopicmapRenderer(TopicmapRenderer renderer) {
275        logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
276        topicmapRenderers.put(renderer.getUri(), renderer);
277    }
278
279    // ---
280
281    @Override
282    public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
283        logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
284        viewmodelCustomizers.add(customizer);
285    }
286
287    @Override
288    public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
289        logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
290        if (!viewmodelCustomizers.remove(customizer)) {
291            throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
292        }
293    }
294
295    // ---
296
297    // Note: not part of topicmaps service
298    @GET
299    @Path("/{id}")
300    @Produces("text/html")
301    public InputStream getTopicmapInWebclient() {
302        // Note: the path parameter is evaluated at client-side
303        return invokeWebclient();
304    }
305
306    // Note: not part of topicmaps service
307    @GET
308    @Path("/{id}/topic/{topic_id}")
309    @Produces("text/html")
310    public InputStream getTopicmapAndTopicInWebclient() {
311        // Note: the path parameters are evaluated at client-side
312        return invokeWebclient();
313    }
314
315
316
317    // ------------------------------------------------------------------------------------------------- Private Methods
318
319    // --- Fetch ---
320
321    private Map<Long, TopicViewModel> fetchTopics(Topic topicmapTopic, boolean includeChilds) {
322        Map<Long, TopicViewModel> topics = new HashMap();
323        List<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, "dm4.core.default",
324            "dm4.topicmaps.topicmap_topic", null);  // othersTopicTypeUri=null
325        if (includeChilds) {
326            DeepaMehtaUtils.loadChildTopics(relTopics);
327        }
328        for (RelatedTopic topic : relTopics) {
329            topics.put(topic.getId(), createTopicViewModel(topic));
330        }
331        return topics;
332    }
333
334    private Map<Long, AssociationViewModel> fetchAssociations(Topic topicmapTopic) {
335        Map<Long, AssociationViewModel> assocs = new HashMap();
336        List<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT,
337            "dm4.core.default", "dm4.topicmaps.topicmap_association", null);
338        for (RelatedAssociation assoc : relAssocs) {
339            assocs.put(assoc.getId(), mf.newAssociationViewModel(assoc.getModel()));
340        }
341        return assocs;
342    }
343
344    // ---
345
346    private TopicViewModel createTopicViewModel(RelatedTopic topic) {
347        try {
348            ViewProperties viewProps = fetchViewProperties(topic.getRelatingAssociation());
349            invokeViewmodelCustomizers(topic, viewProps);
350            return mf.newTopicViewModel(topic.getModel(), viewProps);
351        } catch (Exception e) {
352            throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e);
353        }
354    }
355
356    // ---
357
358    private Association fetchTopicRefAssociation(long topicmapId, long topicId) {
359        return dm4.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC);
360    }
361
362    private Association fetchAssociationRefAssociation(long topicmapId, long assocId) {
363        return dm4.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
364            ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION);
365    }
366
367    // ---
368
369    private ViewProperties fetchViewProperties(Association mapcontextAssoc) {
370        int x = (Integer) mapcontextAssoc.getProperty(PROP_X);
371        int y = (Integer) mapcontextAssoc.getProperty(PROP_Y);
372        boolean visibility = (Boolean) mapcontextAssoc.getProperty(PROP_VISIBILITY);
373        return new ViewProperties(x, y, visibility);
374    }
375
376    // --- Store ---
377
378    private void storeViewProperties(long topicmapId, long topicId, ViewProperties viewProps) {
379        try {
380            Association assoc = fetchTopicRefAssociation(topicmapId, topicId);
381            if (assoc == null) {
382                throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId);
383            }
384            storeViewProperties(assoc, viewProps);
385        } catch (Exception e) {
386            throw new RuntimeException("Storing view properties of topic " + topicId + " failed " +
387                "(viewProps=" + viewProps + ")", e);
388        }
389    }
390
391    private void storeViewProperties(Association mapcontextAssoc, ViewProperties viewProps) {
392        for (String propUri : viewProps) {
393            mapcontextAssoc.setProperty(propUri, viewProps.get(propUri), false);    // addToIndex = false
394        }
395    }
396
397    // --- Viewmodel Customizers ---
398
399    private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) {
400        for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
401            invokeViewmodelCustomizer(customizer, topic, viewProps);
402        }
403    }
404
405    private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic,
406                                                                           ViewProperties viewProps) {
407        try {
408            customizer.enrichViewProperties(topic, viewProps);
409        } catch (Exception e) {
410            throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
411                "(customizer=\"" + customizer.getClass().getName() + "\")", e);
412        }
413    }
414
415    // --- Topicmap Renderers ---
416
417    private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
418        TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
419        //
420        if (renderer == null) {
421            throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
422        }
423        //
424        return renderer;
425    }
426
427    // ---
428
429    private InputStream invokeWebclient() {
430        return dm4.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html");
431    }
432}