001 package de.deepamehta.plugins.topicmaps; 002 003 import de.deepamehta.plugins.topicmaps.model.TopicmapViewmodel; 004 import de.deepamehta.plugins.topicmaps.model.TopicViewmodel; 005 import de.deepamehta.plugins.topicmaps.model.AssociationViewmodel; 006 import de.deepamehta.plugins.topicmaps.service.TopicmapsService; 007 008 import de.deepamehta.core.Association; 009 import de.deepamehta.core.RelatedAssociation; 010 import de.deepamehta.core.RelatedTopic; 011 import de.deepamehta.core.Topic; 012 import de.deepamehta.core.model.AssociationModel; 013 import de.deepamehta.core.model.AssociationRoleModel; 014 import de.deepamehta.core.model.ChildTopicsModel; 015 import de.deepamehta.core.model.TopicModel; 016 import de.deepamehta.core.model.TopicRoleModel; 017 import de.deepamehta.core.osgi.PluginActivator; 018 import de.deepamehta.core.service.ResultList; 019 import de.deepamehta.core.service.Transactional; 020 021 import javax.ws.rs.GET; 022 import javax.ws.rs.PUT; 023 import javax.ws.rs.POST; 024 import javax.ws.rs.DELETE; 025 import javax.ws.rs.Path; 026 import javax.ws.rs.PathParam; 027 import javax.ws.rs.QueryParam; 028 import javax.ws.rs.Produces; 029 import javax.ws.rs.Consumes; 030 031 import java.io.InputStream; 032 import java.util.ArrayList; 033 import java.util.HashMap; 034 import java.util.List; 035 import java.util.Map; 036 import java.util.logging.Logger; 037 038 039 040 @Path("/topicmap") 041 @Consumes("application/json") 042 @Produces("application/json") 043 public class TopicmapsPlugin extends PluginActivator implements TopicmapsService { 044 045 // ------------------------------------------------------------------------------------------------------- Constants 046 047 private static final String DEFAULT_TOPICMAP_NAME = "untitled"; 048 private static final String DEFAULT_TOPICMAP_URI = "dm4.topicmaps.default_topicmap"; 049 private static final String DEFAULT_TOPICMAP_RENDERER = "dm4.webclient.default_topicmap_renderer"; 050 051 // association type semantics ### TODO: to be dropped. Model-driven manipulators required. 052 private static final String TOPIC_MAPCONTEXT = "dm4.topicmaps.topic_mapcontext"; 053 private static final String ASSOCIATION_MAPCONTEXT = "dm4.topicmaps.association_mapcontext"; 054 private static final String ROLE_TYPE_TOPICMAP = "dm4.core.default"; 055 private static final String ROLE_TYPE_TOPIC = "dm4.topicmaps.topicmap_topic"; 056 private static final String ROLE_TYPE_ASSOCIATION = "dm4.topicmaps.topicmap_association"; 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 InitializePluginListener would be too late. 071 // The renderer is already needed in the PostInstallPluginListener. 072 registerTopicmapRenderer(new DefaultTopicmapRenderer()); 073 } 074 075 076 077 // *************************************** 078 // *** TopicmapsService Implementation *** 079 // *************************************** 080 081 082 083 @GET 084 @Path("/{id}") 085 @Override 086 public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId, 087 @QueryParam("include_childs") boolean includeChilds) { 088 try { 089 logger.info("Loading topicmap " + topicmapId + " (includeChilds=" + includeChilds + ")"); 090 // Note: a TopicmapViewmodel is not a DeepaMehtaObject. So the JerseyResponseFilter's automatic 091 // child topic loading is not applied. We must load the child topics manually here. 092 Topic topicmapTopic = dms.getTopic(topicmapId).loadChildTopics(); 093 Map<Long, TopicViewmodel> topics = fetchTopics(topicmapTopic, includeChilds); 094 Map<Long, AssociationViewmodel> assocs = fetchAssociations(topicmapTopic); 095 // 096 return new TopicmapViewmodel(topicmapTopic.getModel(), topics, assocs); 097 } catch (Exception e) { 098 throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e); 099 } 100 } 101 102 // --- 103 104 @POST 105 @Path("/{name}/{topicmap_renderer_uri}") 106 @Transactional 107 @Override 108 public Topic createTopicmap(@PathParam("name") String name, 109 @PathParam("topicmap_renderer_uri") String topicmapRendererUri) { 110 return createTopicmap(name, null, topicmapRendererUri); 111 } 112 113 @Override 114 public Topic createTopicmap(String name, String uri, String topicmapRendererUri) { 115 ChildTopicsModel topicmapState = getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(); 116 return dms.createTopic(new TopicModel(uri, "dm4.topicmaps.topicmap", new ChildTopicsModel() 117 .put("dm4.topicmaps.name", name) 118 .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri) 119 .put("dm4.topicmaps.state", topicmapState) 120 )); 121 } 122 123 // --- 124 125 @POST 126 @Path("/{id}/topic/{topic_id}") 127 @Transactional 128 @Override 129 public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 130 ChildTopicsModel viewProps) { 131 try { 132 dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT, 133 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 134 new TopicRoleModel(topicId, ROLE_TYPE_TOPIC), viewProps 135 )); 136 storeCustomViewProperties(topicmapId, topicId, viewProps); 137 } catch (Exception e) { 138 throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " + 139 "(viewProps=" + viewProps + ")", e); 140 } 141 } 142 143 @Override 144 public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) { 145 addTopicToTopicmap(topicmapId, topicId, new StandardViewProperties(x, y, visibility)); 146 } 147 148 @POST 149 @Path("/{id}/association/{assoc_id}") 150 @Transactional 151 @Override 152 public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 153 dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT, 154 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 155 new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION) 156 )); 157 } 158 159 // --- 160 161 @Override 162 public boolean isTopicInTopicmap(long topicmapId, long topicId) { 163 return fetchTopicRefAssociation(topicmapId, topicId) != null; 164 } 165 166 // --- 167 168 @PUT 169 @Path("/{id}/topic/{topic_id}") 170 @Transactional 171 @Override 172 public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 173 ChildTopicsModel viewProps) { 174 try { 175 storeStandardViewProperties(topicmapId, topicId, viewProps); 176 storeCustomViewProperties(topicmapId, topicId, viewProps); 177 } catch (Exception e) { 178 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 179 "(viewProps=" + viewProps + ")", e); 180 } 181 } 182 183 184 @PUT 185 @Path("/{id}/topic/{topic_id}/{x}/{y}") 186 @Transactional 187 @Override 188 public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 189 @PathParam("x") int x, @PathParam("y") int y) { 190 storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(x, y)); 191 } 192 193 @PUT 194 @Path("/{id}/topic/{topic_id}/{visibility}") 195 @Transactional 196 @Override 197 public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 198 @PathParam("visibility") boolean visibility) { 199 storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(visibility)); 200 } 201 202 @DELETE 203 @Path("/{id}/association/{assoc_id}") 204 @Transactional 205 @Override 206 public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 207 fetchAssociationRefAssociation(topicmapId, assocId).delete(); 208 } 209 210 // --- 211 212 @PUT 213 @Path("/{id}") 214 @Transactional 215 @Override 216 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 217 for (ClusterCoords.Entry entry : coords) { 218 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 219 } 220 } 221 222 @PUT 223 @Path("/{id}/translation/{x}/{y}") 224 @Transactional 225 @Override 226 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 227 @PathParam("y") int transY) { 228 try { 229 ChildTopicsModel topicmapState = new ChildTopicsModel() 230 .put("dm4.topicmaps.state", new ChildTopicsModel() 231 .put("dm4.topicmaps.translation", new ChildTopicsModel() 232 .put("dm4.topicmaps.translation_x", transX) 233 .put("dm4.topicmaps.translation_y", transY))); 234 dms.updateTopic(new TopicModel(topicmapId, topicmapState)); 235 } catch (Exception e) { 236 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 237 transX + ", transY=" + transY + ")", e); 238 } 239 } 240 241 // --- 242 243 @Override 244 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 245 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 246 topicmapRenderers.put(renderer.getUri(), renderer); 247 } 248 249 // --- 250 251 @Override 252 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 253 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 254 viewmodelCustomizers.add(customizer); 255 } 256 257 @Override 258 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 259 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 260 if (!viewmodelCustomizers.remove(customizer)) { 261 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 262 } 263 } 264 265 // --- 266 267 // Note: not part of topicmaps service 268 @GET 269 @Path("/{id}") 270 @Produces("text/html") 271 public InputStream getTopicmapInWebclient() { 272 // Note: the path parameter is evaluated at client-side 273 return invokeWebclient(); 274 } 275 276 // Note: not part of topicmaps service 277 @GET 278 @Path("/{id}/topic/{topic_id}") 279 @Produces("text/html") 280 public InputStream getTopicmapAndTopicInWebclient() { 281 // Note: the path parameters are evaluated at client-side 282 return invokeWebclient(); 283 } 284 285 286 287 // **************************** 288 // *** Hook Implementations *** 289 // **************************** 290 291 292 293 @Override 294 public void postInstall() { 295 createTopicmap(DEFAULT_TOPICMAP_NAME, DEFAULT_TOPICMAP_URI, DEFAULT_TOPICMAP_RENDERER); 296 // Note: On post-install we have no workspace cookie. 297 // The workspace assignment is made by the Access Control plugin on all-plugins-active. 298 } 299 300 301 302 // ------------------------------------------------------------------------------------------------- Private Methods 303 304 // --- Fetch --- 305 306 private Map<Long, TopicViewmodel> fetchTopics(Topic topicmapTopic, boolean includeChilds) { 307 Map<Long, TopicViewmodel> topics = new HashMap(); 308 ResultList<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics("dm4.topicmaps.topic_mapcontext", 309 "dm4.core.default", "dm4.topicmaps.topicmap_topic", null, 0); // othersTopicTypeUri=null, maxResultSize=0 310 if (includeChilds) { 311 relTopics.loadChildTopics(); 312 } 313 for (RelatedTopic topic : relTopics) { 314 Association assoc = topic.getRelatingAssociation().loadChildTopics(); 315 ChildTopicsModel viewProps = assoc.getChildTopics().getModel(); 316 invokeViewmodelCustomizers("enrichViewProperties", topic, viewProps); 317 topics.put(topic.getId(), new TopicViewmodel(topic.getModel(), viewProps)); 318 } 319 return topics; 320 } 321 322 private Map<Long, AssociationViewmodel> fetchAssociations(Topic topicmapTopic) { 323 Map<Long, AssociationViewmodel> assocs = new HashMap(); 324 ResultList<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations( 325 "dm4.topicmaps.association_mapcontext", "dm4.core.default", "dm4.topicmaps.topicmap_association", null); 326 for (RelatedAssociation assoc : relAssocs) { 327 assocs.put(assoc.getId(), new AssociationViewmodel(assoc.getModel())); 328 } 329 return assocs; 330 } 331 332 // --- 333 334 private Association fetchTopicRefAssociation(long topicmapId, long topicId) { 335 return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC); 336 } 337 338 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) { 339 return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 340 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION); 341 } 342 343 // --- Store --- 344 345 private void storeStandardViewProperties(long topicmapId, long topicId, ChildTopicsModel viewProps) { 346 fetchTopicRefAssociation(topicmapId, topicId).setChildTopics(viewProps); 347 } 348 349 // ### Note: the topicmapId parameter is not used. Per-topicmap custom view properties not yet supported. 350 private void storeCustomViewProperties(long topicmapId, long topicId, ChildTopicsModel viewProps) { 351 invokeViewmodelCustomizers("storeViewProperties", dms.getTopic(topicId), viewProps); 352 } 353 354 // --- Viewmodel Customizers --- 355 356 private void invokeViewmodelCustomizers(String method, Topic topic, ChildTopicsModel viewProps) { 357 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 358 invokeViewmodelCustomizer(customizer, method, topic, viewProps); 359 } 360 } 361 362 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, String method, 363 Topic topic, ChildTopicsModel viewProps) { 364 try { 365 // we don't want use reflection here for performance reasons 366 if (method.equals("enrichViewProperties")) { 367 customizer.enrichViewProperties(topic, viewProps); 368 } else if (method.equals("storeViewProperties")) { 369 customizer.storeViewProperties(topic, viewProps); 370 } else { 371 throw new RuntimeException("\"" + method + "\" is an unexpected method"); 372 } 373 } catch (Exception e) { 374 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 375 "(customizer=\"" + customizer.getClass().getName() + "\", method=\"" + method + "\")", e); 376 } 377 } 378 379 // --- Topicmap Renderers --- 380 381 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 382 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 383 // 384 if (renderer == null) { 385 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 386 } 387 // 388 return renderer; 389 } 390 391 // --- 392 393 private InputStream invokeWebclient() { 394 return dms.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html"); 395 } 396 397 // --------------------------------------------------------------------------------------------- Private Inner Class 398 399 private class StandardViewProperties extends ChildTopicsModel { 400 401 private StandardViewProperties(int x, int y, boolean visibility) { 402 put(x, y); 403 put(visibility); 404 } 405 406 private StandardViewProperties(int x, int y) { 407 put(x, y); 408 } 409 410 411 private StandardViewProperties(boolean visibility) { 412 put(visibility); 413 } 414 415 // --- 416 417 private void put(int x, int y) { 418 put("dm4.topicmaps.x", x); 419 put("dm4.topicmaps.y", y); 420 } 421 422 private void put(boolean visibility) { 423 put("dm4.topicmaps.visibility", visibility); 424 } 425 } 426 }