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