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}