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