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