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