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