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            // Note: a mapcontext association belongs to the system (it has no workspace assignment).
234            // Deletion is possible only via privileged operation.
235            dm4.getAccessControl().deleteAssociationMapcontext(assoc);
236        } catch (Exception e) {
237            throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ",
238                e);
239        }
240    }
241
242    // ---
243
244    @PUT
245    @Path("/{id}")
246    @Transactional
247    @Override
248    public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) {
249        for (ClusterCoords.Entry entry : coords) {
250            setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y);
251        }
252    }
253
254    @PUT
255    @Path("/{id}/translation/{x}/{y}")
256    @Transactional
257    @Override
258    public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX,
259                                                                         @PathParam("y") int transY) {
260        try {
261            ChildTopicsModel topicmapState = mf.newChildTopicsModel()
262                .put("dm4.topicmaps.state", mf.newChildTopicsModel()
263                    .put("dm4.topicmaps.translation", mf.newChildTopicsModel()
264                        .put("dm4.topicmaps.translation_x", transX)
265                        .put("dm4.topicmaps.translation_y", transY)));
266            dm4.updateTopic(mf.newTopicModel(topicmapId, topicmapState));
267        } catch (Exception e) {
268            throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" +
269                transX + ", transY=" + transY + ")", e);
270        }
271    }
272
273    // ---
274
275    @Override
276    public void registerTopicmapRenderer(TopicmapRenderer renderer) {
277        logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\"");
278        topicmapRenderers.put(renderer.getUri(), renderer);
279    }
280
281    // ---
282
283    @Override
284    public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) {
285        logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
286        viewmodelCustomizers.add(customizer);
287    }
288
289    @Override
290    public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) {
291        logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\"");
292        if (!viewmodelCustomizers.remove(customizer)) {
293            throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")");
294        }
295    }
296
297    // ---
298
299    // Note: not part of topicmaps service
300    @GET
301    @Path("/{id}")
302    @Produces("text/html")
303    public InputStream getTopicmapInWebclient() {
304        // Note: the path parameter is evaluated at client-side
305        return invokeWebclient();
306    }
307
308    // Note: not part of topicmaps service
309    @GET
310    @Path("/{id}/topic/{topic_id}")
311    @Produces("text/html")
312    public InputStream getTopicmapAndTopicInWebclient() {
313        // Note: the path parameters are evaluated at client-side
314        return invokeWebclient();
315    }
316
317
318
319    // ------------------------------------------------------------------------------------------------- Private Methods
320
321    // --- Fetch ---
322
323    private Map<Long, TopicViewModel> fetchTopics(Topic topicmapTopic, boolean includeChilds) {
324        Map<Long, TopicViewModel> topics = new HashMap();
325        List<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, "dm4.core.default",
326            "dm4.topicmaps.topicmap_topic", null);  // othersTopicTypeUri=null
327        if (includeChilds) {
328            DeepaMehtaUtils.loadChildTopics(relTopics);
329        }
330        for (RelatedTopic topic : relTopics) {
331            topics.put(topic.getId(), createTopicViewModel(topic));
332        }
333        return topics;
334    }
335
336    private Map<Long, AssociationViewModel> fetchAssociations(Topic topicmapTopic) {
337        Map<Long, AssociationViewModel> assocs = new HashMap();
338        List<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT,
339            "dm4.core.default", "dm4.topicmaps.topicmap_association", null);
340        for (RelatedAssociation assoc : relAssocs) {
341            assocs.put(assoc.getId(), mf.newAssociationViewModel(assoc.getModel()));
342        }
343        return assocs;
344    }
345
346    // ---
347
348    private TopicViewModel createTopicViewModel(RelatedTopic topic) {
349        try {
350            ViewProperties viewProps = fetchViewProperties(topic.getRelatingAssociation());
351            invokeViewmodelCustomizers(topic, viewProps);
352            return mf.newTopicViewModel(topic.getModel(), viewProps);
353        } catch (Exception e) {
354            throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e);
355        }
356    }
357
358    // ---
359
360    private Association fetchTopicRefAssociation(long topicmapId, long topicId) {
361        return dm4.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC);
362    }
363
364    private Association fetchAssociationRefAssociation(long topicmapId, long assocId) {
365        return dm4.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId,
366            ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION);
367    }
368
369    // ---
370
371    private ViewProperties fetchViewProperties(Association mapcontextAssoc) {
372        int x = (Integer) mapcontextAssoc.getProperty(PROP_X);
373        int y = (Integer) mapcontextAssoc.getProperty(PROP_Y);
374        boolean visibility = (Boolean) mapcontextAssoc.getProperty(PROP_VISIBILITY);
375        return new ViewProperties(x, y, visibility);
376    }
377
378    // --- Store ---
379
380    private void storeViewProperties(long topicmapId, long topicId, ViewProperties viewProps) {
381        try {
382            Association assoc = fetchTopicRefAssociation(topicmapId, topicId);
383            if (assoc == null) {
384                throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId);
385            }
386            storeViewProperties(assoc, viewProps);
387        } catch (Exception e) {
388            throw new RuntimeException("Storing view properties of topic " + topicId + " failed " +
389                "(viewProps=" + viewProps + ")", e);
390        }
391    }
392
393    private void storeViewProperties(Association mapcontextAssoc, ViewProperties viewProps) {
394        for (String propUri : viewProps) {
395            mapcontextAssoc.setProperty(propUri, viewProps.get(propUri), false);    // addToIndex = false
396        }
397    }
398
399    // --- Viewmodel Customizers ---
400
401    private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) {
402        for (ViewmodelCustomizer customizer : viewmodelCustomizers) {
403            invokeViewmodelCustomizer(customizer, topic, viewProps);
404        }
405    }
406
407    private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic,
408                                                                           ViewProperties viewProps) {
409        try {
410            customizer.enrichViewProperties(topic, viewProps);
411        } catch (Exception e) {
412            throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " +
413                "(customizer=\"" + customizer.getClass().getName() + "\")", e);
414        }
415    }
416
417    // --- Topicmap Renderers ---
418
419    private TopicmapRenderer getTopicmapRenderer(String rendererUri) {
420        TopicmapRenderer renderer = topicmapRenderers.get(rendererUri);
421        //
422        if (renderer == null) {
423            throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer");
424        }
425        //
426        return renderer;
427    }
428
429    // ---
430
431    private InputStream invokeWebclient() {
432        return dm4.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html");
433    }
434}