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