001package systems.dmx.topicmaps; 002 003import systems.dmx.topicmaps.model.TopicmapViewmodel; 004 005import systems.dmx.core.Association; 006import systems.dmx.core.RelatedAssociation; 007import systems.dmx.core.RelatedTopic; 008import systems.dmx.core.Topic; 009import systems.dmx.core.model.AssociationModel; 010import systems.dmx.core.model.AssociationRoleModel; 011import systems.dmx.core.model.ChildTopicsModel; 012import systems.dmx.core.model.TopicModel; 013import systems.dmx.core.model.TopicRoleModel; 014import systems.dmx.core.model.topicmaps.AssociationViewModel; 015import systems.dmx.core.model.topicmaps.TopicViewModel; 016import systems.dmx.core.model.topicmaps.ViewProperties; 017import systems.dmx.core.osgi.PluginActivator; 018import systems.dmx.core.service.Transactional; 019import systems.dmx.core.util.DMXUtils; 020import systems.dmx.core.util.IdList; 021 022import org.codehaus.jettison.json.JSONObject; 023 024import javax.ws.rs.GET; 025import javax.ws.rs.PUT; 026import javax.ws.rs.POST; 027import javax.ws.rs.DELETE; 028import javax.ws.rs.Consumes; 029import javax.ws.rs.Path; 030import javax.ws.rs.PathParam; 031import javax.ws.rs.Produces; 032import javax.ws.rs.QueryParam; 033import javax.ws.rs.core.Context; 034 035import javax.servlet.http.HttpServletRequest; 036 037import java.io.InputStream; 038import java.util.ArrayList; 039import java.util.HashMap; 040import java.util.List; 041import java.util.Map; 042import java.util.concurrent.Callable; 043import java.util.logging.Level; 044import java.util.logging.Logger; 045 046 047 048@Path("/topicmap") 049@Consumes("application/json") 050@Produces("application/json") 051public class TopicmapsPlugin extends PluginActivator implements TopicmapsService { 052 053 // ------------------------------------------------------------------------------------------------------- Constants 054 055 // association type semantics ### TODO: to be dropped. Model-driven manipulators required. 056 private static final String TOPIC_MAPCONTEXT = "dmx.topicmaps.topic_mapcontext"; 057 private static final String ASSOCIATION_MAPCONTEXT = "dmx.topicmaps.association_mapcontext"; 058 private static final String ROLE_TYPE_TOPICMAP = "dmx.core.default"; 059 private static final String ROLE_TYPE_TOPIC = "dmx.topicmaps.topicmap_topic"; 060 private static final String ROLE_TYPE_ASSOCIATION = "dmx.topicmaps.topicmap_association"; 061 062 private static final String PROP_X = "dmx.topicmaps.x"; 063 private static final String PROP_Y = "dmx.topicmaps.y"; 064 private static final String PROP_VISIBILITY = "dmx.topicmaps.visibility"; 065 private static final String PROP_PINNED = "dmx.topicmaps.pinned"; 066 067 // ---------------------------------------------------------------------------------------------- Instance Variables 068 069 private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap(); 070 private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList(); 071 private Messenger me = new Messenger("systems.dmx.webclient"); 072 073 @Context 074 private HttpServletRequest request; 075 076 private Logger logger = Logger.getLogger(getClass().getName()); 077 078 // -------------------------------------------------------------------------------------------------- Public Methods 079 080 081 082 public TopicmapsPlugin() { 083 // Note: registering the default renderer in the init() hook would be too late. 084 // The renderer is already needed at install-in-DB time ### Still true? Use preInstall() hook? 085 registerTopicmapRenderer(new DefaultTopicmapRenderer()); 086 } 087 088 089 090 // *************************************** 091 // *** TopicmapsService Implementation *** 092 // *************************************** 093 094 095 096 @POST 097 @Transactional 098 @Override 099 public Topic createTopicmap(@QueryParam("name") String name, 100 @QueryParam("renderer_uri") String topicmapRendererUri, 101 @QueryParam("private") boolean isPrivate) { 102 logger.info("Creating topicmap \"" + name + "\" (topicmapRendererUri=\"" + topicmapRendererUri + 103 "\", isPrivate=" + isPrivate +")"); 104 Topic topicmapTopic = dmx.createTopic(mf.newTopicModel("dmx.topicmaps.topicmap", mf.newChildTopicsModel() 105 .put("dmx.topicmaps.name", name) 106 .put("dmx.topicmaps.topicmap_renderer_uri", topicmapRendererUri) 107 .put("dmx.topicmaps.private", isPrivate) 108 .put("dmx.topicmaps.state", getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(mf)))); 109 me.newTopicmap(topicmapTopic); // FIXME: broadcast to eligible users only 110 return topicmapTopic; 111 } 112 113 // --- 114 115 @GET 116 @Path("/{id}") 117 @Override 118 public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId, 119 @QueryParam("include_childs") boolean includeChilds) { 120 try { 121 logger.info("Loading topicmap " + topicmapId + " (includeChilds=" + includeChilds + ")"); 122 // Note: a TopicmapViewmodel is not a DMXObject. So the JerseyResponseFilter's automatic 123 // child topic loading is not applied. We must load the child topics manually here. 124 Topic topicmapTopic = dmx.getTopic(topicmapId).loadChildTopics(); 125 Map<Long, TopicViewModel> topics = fetchTopics(topicmapTopic, includeChilds); 126 Map<Long, AssociationViewModel> assocs = fetchAssociations(topicmapTopic); 127 // 128 return new TopicmapViewmodel(topicmapTopic.getModel(), topics, assocs); 129 } catch (Exception e) { 130 throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e); 131 } 132 } 133 134 @Override 135 public boolean isTopicInTopicmap(long topicmapId, long topicId) { 136 return fetchTopicMapcontext(topicmapId, topicId) != null; 137 } 138 139 @Override 140 public boolean isAssociationInTopicmap(long topicmapId, long assocId) { 141 return fetchAssociationMapcontext(topicmapId, assocId) != null; 142 } 143 144 // --- 145 146 @POST 147 @Path("/{id}/topic/{topic_id}") 148 @Transactional 149 @Override 150 public void addTopicToTopicmap(@PathParam("id") final long topicmapId, 151 @PathParam("topic_id") final long topicId, final ViewProperties viewProps) { 152 try { 153 // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable 154 dmx.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() { // throws Exception 155 @Override 156 public Void call() { 157 if (isTopicInTopicmap(topicmapId, topicId)) { 158 throw new RuntimeException("Topic " + topicId + " already added to topicmap" + topicmapId); 159 } 160 createTopicMapcontext(topicmapId, topicId, viewProps); 161 return null; 162 } 163 }); 164 } catch (Exception e) { 165 throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " + 166 "(viewProps=" + viewProps + ")", e); 167 } 168 } 169 170 @Override 171 public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) { 172 addTopicToTopicmap(topicmapId, topicId, new ViewProperties(x, y, visibility, false)); // pinned=false 173 } 174 175 @POST 176 @Path("/{id}/association/{assoc_id}") 177 @Transactional 178 @Override 179 public void addAssociationToTopicmap(@PathParam("id") final long topicmapId, 180 @PathParam("assoc_id") final long assocId, final ViewProperties viewProps) { 181 try { 182 // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable 183 dmx.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() { // throws Exception 184 @Override 185 public Void call() { 186 if (isAssociationInTopicmap(topicmapId, assocId)) { 187 throw new RuntimeException("Association " + assocId + " already added to topicmap " + 188 topicmapId); 189 } 190 createAssociationMapcontext(topicmapId, assocId, viewProps); 191 return null; 192 } 193 }); 194 } catch (Exception e) { 195 throw new RuntimeException("Adding association " + assocId + " to topicmap " + topicmapId + " failed " + 196 "(viewProps=" + viewProps + ")", e); 197 } 198 } 199 200 @POST 201 @Path("/{id}/topic/{topic_id}/association/{assoc_id}") 202 @Transactional 203 @Override 204 public void addRelatedTopicToTopicmap(@PathParam("id") final long topicmapId, 205 @PathParam("topic_id") final long topicId, 206 @PathParam("assoc_id") final long assocId, final ViewProperties viewProps) { 207 try { 208 // Note: a Mapcontext association must have no workspace assignment as it is not user-deletable 209 dmx.getAccessControl().runWithoutWorkspaceAssignment(new Callable<Void>() { // throws Exception 210 @Override 211 public Void call() { 212 // 1) add topic 213 Association topicMapcontext = fetchTopicMapcontext(topicmapId, topicId); 214 if (topicMapcontext == null) { 215 createTopicMapcontext(topicmapId, topicId, viewProps); 216 } else { 217 if (!visibility(topicMapcontext)) { 218 setTopicVisibility(topicmapId, topicId, true); 219 } 220 } 221 // 2) add association 222 // Note: it is an error if the association is already in the topicmap. In this case the topic is 223 // already in the topicmap too, and the Webclient would not send the request in the first place. 224 // ### TODO: rethink method contract. Do it analoguous to "add topic"? 225 addAssociationToTopicmap(topicmapId, assocId, new ViewProperties().put(PROP_PINNED, false)); 226 return null; 227 } 228 }); 229 } catch (Exception e) { 230 throw new RuntimeException("Adding related topic " + topicId + " (assocId=" + assocId + ") to topicmap " + 231 topicmapId + " failed (viewProps=" + viewProps + ")", e); 232 } 233 } 234 235 // --- 236 237 @PUT 238 @Path("/{id}/topic/{topic_id}") 239 @Transactional 240 @Override 241 public void setTopicViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 242 ViewProperties viewProps) { 243 storeTopicViewProperties(topicmapId, topicId, viewProps); 244 } 245 246 @PUT 247 @Path("/{id}/association/{assoc_id}") 248 @Transactional 249 @Override 250 public void setAssociationViewProperties(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId, 251 ViewProperties viewProps) { 252 storeAssociationViewProperties(topicmapId, assocId, viewProps); 253 } 254 255 @PUT 256 @Path("/{id}/topic/{topic_id}/{x}/{y}") 257 @Transactional 258 @Override 259 public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 260 @PathParam("x") int x, @PathParam("y") int y) { 261 try { 262 storeTopicViewProperties(topicmapId, topicId, new ViewProperties(x, y)); 263 me.setTopicPosition(topicmapId, topicId, x, y); 264 } catch (Exception e) { 265 throw new RuntimeException("Setting position of topic " + topicId + " in topicmap " + topicmapId + 266 " failed ", e); 267 } 268 } 269 270 @PUT 271 @Path("/{id}/topic/{topic_id}/{visibility}") 272 @Transactional 273 @Override 274 public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 275 @PathParam("visibility") boolean visibility) { 276 try { 277 // remove associations 278 if (!visibility) { 279 for (Association assoc : dmx.getTopic(topicId).getAssociations()) { 280 Association assocMapcontext = fetchAssociationMapcontext(topicmapId, assoc.getId()); 281 if (assocMapcontext != null) { 282 deleteAssociationMapcontext(assocMapcontext); 283 } 284 } 285 } 286 // show/hide topic 287 storeTopicViewProperties(topicmapId, topicId, new ViewProperties(visibility)); 288 // send message 289 me.setTopicVisibility(topicmapId, topicId, visibility); 290 } catch (Exception e) { 291 throw new RuntimeException("Setting visibility of topic " + topicId + " in topicmap " + topicmapId + 292 " failed ", e); 293 } 294 } 295 296 @DELETE 297 @Path("/{id}/association/{assoc_id}") 298 @Transactional 299 @Override 300 public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 301 try { 302 Association assocMapcontext = fetchAssociationMapcontext(topicmapId, assocId); 303 // Note: idempotence of remove-assoc-from-topicmap is needed for delete-muti 304 if (assocMapcontext != null) { 305 deleteAssociationMapcontext(assocMapcontext); 306 me.removeAssociationFromTopicmap(topicmapId, assocId); 307 } 308 } catch (Exception e) { 309 throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ", 310 e); 311 } 312 } 313 314 // --- 315 316 @PUT 317 @Path("/{id}/topics/{topicIds}/visibility/false") 318 @Transactional 319 @Override 320 public void hideTopics(@PathParam("id") long topicmapId, @PathParam("topicIds") IdList topicIds) { 321 hideMulti(topicmapId, topicIds, new IdList()); 322 } 323 324 @PUT 325 @Path("/{id}/assocs/{assocIds}/visibility/false") 326 @Transactional 327 @Override 328 public void hideAssocs(@PathParam("id") long topicmapId, @PathParam("assocIds") IdList assocIds) { 329 hideMulti(topicmapId, new IdList(), assocIds); 330 } 331 332 @PUT 333 @Path("/{id}/topics/{topicIds}/assocs/{assocIds}/visibility/false") 334 @Transactional 335 @Override 336 public void hideMulti(@PathParam("id") long topicmapId, @PathParam("topicIds") IdList topicIds, 337 @PathParam("assocIds") IdList assocIds) { 338 logger.info("topicmapId=" + topicmapId + ", topicIds=" + topicIds + ", assocIds=" + assocIds); 339 for (long id : topicIds) { 340 setTopicVisibility(topicmapId, id, false); 341 } 342 for (long id : assocIds) { 343 removeAssociationFromTopicmap(topicmapId, id); 344 } 345 } 346 347 // --- 348 349 @PUT 350 @Path("/{id}") 351 @Transactional 352 @Override 353 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 354 for (ClusterCoords.Entry entry : coords) { 355 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 356 } 357 } 358 359 @PUT 360 @Path("/{id}/translation/{x}/{y}") 361 @Transactional 362 @Override 363 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 364 @PathParam("y") int transY) { 365 try { 366 ChildTopicsModel topicmapState = mf.newChildTopicsModel() 367 .put("dmx.topicmaps.state", mf.newChildTopicsModel() 368 .put("dmx.topicmaps.translation", mf.newChildTopicsModel() 369 .put("dmx.topicmaps.translation_x", transX) 370 .put("dmx.topicmaps.translation_y", transY))); 371 dmx.updateTopic(mf.newTopicModel(topicmapId, topicmapState)); 372 } catch (Exception e) { 373 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 374 transX + ", transY=" + transY + ")", e); 375 } 376 } 377 378 // --- 379 380 @Override 381 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 382 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 383 topicmapRenderers.put(renderer.getUri(), renderer); 384 } 385 386 // --- 387 388 @Override 389 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 390 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 391 viewmodelCustomizers.add(customizer); 392 } 393 394 @Override 395 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 396 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 397 if (!viewmodelCustomizers.remove(customizer)) { 398 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 399 } 400 } 401 402 // --- 403 404 // Note: not part of topicmaps service 405 @GET 406 @Path("/{id}") 407 @Produces("text/html") 408 public InputStream getTopicmapInWebclient() { 409 // Note: the path parameter is evaluated at client-side 410 return invokeWebclient(); 411 } 412 413 // Note: not part of topicmaps service 414 @GET 415 @Path("/{id}/topic/{topic_id}") 416 @Produces("text/html") 417 public InputStream getTopicmapAndTopicInWebclient() { 418 // Note: the path parameters are evaluated at client-side 419 return invokeWebclient(); 420 } 421 422 423 424 // ------------------------------------------------------------------------------------------------- Private Methods 425 426 // --- Fetch --- 427 428 private Map<Long, TopicViewModel> fetchTopics(Topic topicmapTopic, boolean includeChilds) { 429 Map<Long, TopicViewModel> topics = new HashMap(); 430 List<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, "dmx.core.default", 431 "dmx.topicmaps.topicmap_topic", null); // othersTopicTypeUri=null 432 if (includeChilds) { 433 DMXUtils.loadChildTopics(relTopics); 434 } 435 for (RelatedTopic topic : relTopics) { 436 topics.put(topic.getId(), createTopicViewModel(topic)); 437 } 438 return topics; 439 } 440 441 private Map<Long, AssociationViewModel> fetchAssociations(Topic topicmapTopic) { 442 Map<Long, AssociationViewModel> assocs = new HashMap(); 443 List<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT, 444 "dmx.core.default", "dmx.topicmaps.topicmap_association", null); 445 for (RelatedAssociation assoc : relAssocs) { 446 assocs.put(assoc.getId(), createAssocViewModel(assoc)); 447 } 448 return assocs; 449 } 450 451 // --- 452 453 private TopicViewModel createTopicViewModel(RelatedTopic topic) { 454 try { 455 ViewProperties viewProps = fetchTopicViewProperties(topic.getRelatingAssociation()); 456 invokeViewmodelCustomizers(topic, viewProps); 457 return mf.newTopicViewModel(topic.getModel(), viewProps); 458 } catch (Exception e) { 459 throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e); 460 } 461 } 462 463 private AssociationViewModel createAssocViewModel(RelatedAssociation assoc) { 464 try { 465 ViewProperties viewProps = fetchAssocViewProperties(assoc.getRelatingAssociation()); 466 // invokeViewmodelCustomizers(assoc, viewProps); // TODO: assoc customizers? 467 return mf.newAssociationViewModel(assoc.getModel(), viewProps); 468 } catch (Exception e) { 469 throw new RuntimeException("Creating viewmodel for association " + assoc.getId() + " failed", e); 470 } 471 } 472 473 // --- 474 475 private Association fetchTopicMapcontext(long topicmapId, long topicId) { 476 return dmx.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC); 477 } 478 479 private Association fetchAssociationMapcontext(long topicmapId, long assocId) { 480 return dmx.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 481 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION); 482 } 483 484 // --- 485 486 private void createTopicMapcontext(long topicmapId, long topicId, ViewProperties viewProps) { 487 Association topicMapcontext = dmx.createAssociation(mf.newAssociationModel(TOPIC_MAPCONTEXT, 488 mf.newTopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 489 mf.newTopicRoleModel(topicId, ROLE_TYPE_TOPIC) 490 )); 491 storeViewProperties(topicMapcontext, viewProps); 492 // 493 TopicViewModel topic = mf.newTopicViewModel(dmx.getTopic(topicId).getModel(), viewProps); 494 me.addTopicToTopicmap(topicmapId, topic); 495 } 496 497 private void createAssociationMapcontext(long topicmapId, long assocId, ViewProperties viewProps) { 498 Association assocMapcontext = dmx.createAssociation(mf.newAssociationModel(ASSOCIATION_MAPCONTEXT, 499 mf.newTopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 500 mf.newAssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION) 501 )); 502 storeViewProperties(assocMapcontext, viewProps); 503 // 504 AssociationModel assoc = dmx.getAssociation(assocId).getModel(); // TODO: view props 505 me.addAssociationToTopicmap(topicmapId, assoc); 506 } 507 508 // --- 509 510 private void deleteAssociationMapcontext(Association assocMapcontext) { 511 // Note: a mapcontext association has no workspace assignment -- it belongs to the system. 512 // Deleting a mapcontext association is a privileged operation. 513 dmx.getAccessControl().deleteAssociationMapcontext(assocMapcontext); 514 } 515 516 // --- 517 518 private ViewProperties fetchTopicViewProperties(Association topicMapcontext) { 519 return new ViewProperties( 520 (Integer) topicMapcontext.getProperty(PROP_X), 521 (Integer) topicMapcontext.getProperty(PROP_Y), 522 visibility(topicMapcontext), 523 pinned(topicMapcontext) 524 ); 525 } 526 527 private ViewProperties fetchAssocViewProperties(Association assocMapcontext) { 528 return new ViewProperties().put(PROP_PINNED, pinned(assocMapcontext)); 529 } 530 531 private boolean visibility(Association topicMapcontext) { 532 return (Boolean) topicMapcontext.getProperty(PROP_VISIBILITY); 533 } 534 535 private boolean pinned(Association mapcontext) { 536 return (Boolean) mapcontext.getProperty(PROP_PINNED); 537 } 538 539 // --- Store --- 540 541 /** 542 * Convenience. 543 */ 544 private void storeTopicViewProperties(long topicmapId, long topicId, ViewProperties viewProps) { 545 try { 546 Association topicMapcontext = fetchTopicMapcontext(topicmapId, topicId); 547 if (topicMapcontext == null) { 548 throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId); 549 } 550 storeViewProperties(topicMapcontext, viewProps); 551 } catch (Exception e) { 552 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 553 "(viewProps=" + viewProps + ")", e); 554 } 555 } 556 557 /** 558 * Convenience. 559 */ 560 private void storeAssociationViewProperties(long topicmapId, long assocId, ViewProperties viewProps) { 561 try { 562 Association assocMapcontext = fetchAssociationMapcontext(topicmapId, assocId); 563 if (assocMapcontext == null) { 564 throw new RuntimeException("Association " + assocId + " is not contained in topicmap " + topicmapId); 565 } 566 storeViewProperties(assocMapcontext, viewProps); 567 } catch (Exception e) { 568 throw new RuntimeException("Storing view properties of association " + assocId + " failed " + 569 "(viewProps=" + viewProps + ")", e); 570 } 571 } 572 573 private void storeViewProperties(Association mapcontext, ViewProperties viewProps) { 574 for (String propUri : viewProps) { 575 mapcontext.setProperty(propUri, viewProps.get(propUri), false); // addToIndex = false 576 } 577 } 578 579 // --- Viewmodel Customizers --- 580 581 private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) { 582 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 583 invokeViewmodelCustomizer(customizer, topic, viewProps); 584 } 585 } 586 587 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic, 588 ViewProperties viewProps) { 589 try { 590 customizer.enrichViewProperties(topic, viewProps); 591 } catch (Exception e) { 592 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 593 "(customizer=\"" + customizer.getClass().getName() + "\")", e); 594 } 595 } 596 597 // --- Topicmap Renderers --- 598 599 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 600 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 601 // 602 if (renderer == null) { 603 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 604 } 605 // 606 return renderer; 607 } 608 609 // --- 610 611 private InputStream invokeWebclient() { 612 return dmx.getPlugin("systems.dmx.webclient").getStaticResource("/web/index.html"); 613 } 614 615 // ------------------------------------------------------------------------------------------------- Private Classes 616 617 private class Messenger { 618 619 private String pluginUri; 620 621 private Messenger(String pluginUri) { 622 this.pluginUri = pluginUri; 623 } 624 625 // --- 626 627 private void newTopicmap(Topic topicmapTopic) { 628 try { 629 messageToAllButOne(new JSONObject() 630 .put("type", "newTopicmap") 631 .put("args", new JSONObject() 632 .put("topicmapTopic", topicmapTopic.toJSON()) 633 ) 634 ); 635 } catch (Exception e) { 636 logger.log(Level.WARNING, "Error while sending a \"newTopicmap\" message:", e); 637 } 638 } 639 640 private void addTopicToTopicmap(long topicmapId, TopicViewModel topic) { 641 try { 642 messageToAllButOne(new JSONObject() 643 .put("type", "addTopicToTopicmap") 644 .put("args", new JSONObject() 645 .put("topicmapId", topicmapId) 646 .put("viewTopic", topic.toJSON()) 647 ) 648 ); 649 } catch (Exception e) { 650 logger.log(Level.WARNING, "Error while sending a \"addTopicToTopicmap\" message:", e); 651 } 652 } 653 654 private void addAssociationToTopicmap(long topicmapId, AssociationModel assoc) { 655 try { 656 messageToAllButOne(new JSONObject() 657 .put("type", "addAssocToTopicmap") 658 .put("args", new JSONObject() 659 .put("topicmapId", topicmapId) 660 .put("assoc", assoc.toJSON()) 661 ) 662 ); 663 } catch (Exception e) { 664 logger.log(Level.WARNING, "Error while sending a \"addAssocToTopicmap\" message:", e); 665 } 666 } 667 668 private void setTopicPosition(long topicmapId, long topicId, int x, int y) { 669 try { 670 messageToAllButOne(new JSONObject() 671 .put("type", "setTopicPosition") 672 .put("args", new JSONObject() 673 .put("topicmapId", topicmapId) 674 .put("topicId", topicId) 675 .put("pos", new JSONObject() 676 .put("x", x) 677 .put("y", y) 678 ) 679 ) 680 ); 681 } catch (Exception e) { 682 logger.log(Level.WARNING, "Error while sending a \"setTopicPosition\" message:", e); 683 } 684 } 685 686 private void setTopicVisibility(long topicmapId, long topicId, boolean visibility) { 687 try { 688 messageToAllButOne(new JSONObject() 689 .put("type", "setTopicVisibility") 690 .put("args", new JSONObject() 691 .put("topicmapId", topicmapId) 692 .put("topicId", topicId) 693 .put("visibility", visibility) 694 ) 695 ); 696 } catch (Exception e) { 697 logger.log(Level.WARNING, "Error while sending a \"setTopicVisibility\" message:", e); 698 } 699 } 700 701 private void removeAssociationFromTopicmap(long topicmapId, long assocId) { 702 try { 703 messageToAllButOne(new JSONObject() 704 .put("type", "removeAssocFromTopicmap") 705 .put("args", new JSONObject() 706 .put("topicmapId", topicmapId) 707 .put("assocId", assocId) 708 ) 709 ); 710 } catch (Exception e) { 711 logger.log(Level.WARNING, "Error while sending a \"removeAssocFromTopicmap\" message:", e); 712 } 713 } 714 715 // --- 716 717 private void messageToAllButOne(JSONObject message) { 718 dmx.getWebSocketsService().messageToAllButOne(request, pluginUri, message.toString()); 719 } 720 } 721}