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 assoc.delete(); 234 } catch (Exception e) { 235 throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ", 236 e); 237 } 238 } 239 240 // --- 241 242 @PUT 243 @Path("/{id}") 244 @Transactional 245 @Override 246 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 247 for (ClusterCoords.Entry entry : coords) { 248 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 249 } 250 } 251 252 @PUT 253 @Path("/{id}/translation/{x}/{y}") 254 @Transactional 255 @Override 256 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 257 @PathParam("y") int transY) { 258 try { 259 ChildTopicsModel topicmapState = mf.newChildTopicsModel() 260 .put("dm4.topicmaps.state", mf.newChildTopicsModel() 261 .put("dm4.topicmaps.translation", mf.newChildTopicsModel() 262 .put("dm4.topicmaps.translation_x", transX) 263 .put("dm4.topicmaps.translation_y", transY))); 264 dm4.updateTopic(mf.newTopicModel(topicmapId, topicmapState)); 265 } catch (Exception e) { 266 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 267 transX + ", transY=" + transY + ")", e); 268 } 269 } 270 271 // --- 272 273 @Override 274 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 275 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 276 topicmapRenderers.put(renderer.getUri(), renderer); 277 } 278 279 // --- 280 281 @Override 282 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 283 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 284 viewmodelCustomizers.add(customizer); 285 } 286 287 @Override 288 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 289 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 290 if (!viewmodelCustomizers.remove(customizer)) { 291 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 292 } 293 } 294 295 // --- 296 297 // Note: not part of topicmaps service 298 @GET 299 @Path("/{id}") 300 @Produces("text/html") 301 public InputStream getTopicmapInWebclient() { 302 // Note: the path parameter is evaluated at client-side 303 return invokeWebclient(); 304 } 305 306 // Note: not part of topicmaps service 307 @GET 308 @Path("/{id}/topic/{topic_id}") 309 @Produces("text/html") 310 public InputStream getTopicmapAndTopicInWebclient() { 311 // Note: the path parameters are evaluated at client-side 312 return invokeWebclient(); 313 } 314 315 316 317 // ------------------------------------------------------------------------------------------------- Private Methods 318 319 // --- Fetch --- 320 321 private Map<Long, TopicViewModel> fetchTopics(Topic topicmapTopic, boolean includeChilds) { 322 Map<Long, TopicViewModel> topics = new HashMap(); 323 List<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, "dm4.core.default", 324 "dm4.topicmaps.topicmap_topic", null); // othersTopicTypeUri=null 325 if (includeChilds) { 326 DeepaMehtaUtils.loadChildTopics(relTopics); 327 } 328 for (RelatedTopic topic : relTopics) { 329 topics.put(topic.getId(), createTopicViewModel(topic)); 330 } 331 return topics; 332 } 333 334 private Map<Long, AssociationViewModel> fetchAssociations(Topic topicmapTopic) { 335 Map<Long, AssociationViewModel> assocs = new HashMap(); 336 List<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT, 337 "dm4.core.default", "dm4.topicmaps.topicmap_association", null); 338 for (RelatedAssociation assoc : relAssocs) { 339 assocs.put(assoc.getId(), mf.newAssociationViewModel(assoc.getModel())); 340 } 341 return assocs; 342 } 343 344 // --- 345 346 private TopicViewModel createTopicViewModel(RelatedTopic topic) { 347 try { 348 ViewProperties viewProps = fetchViewProperties(topic.getRelatingAssociation()); 349 invokeViewmodelCustomizers(topic, viewProps); 350 return mf.newTopicViewModel(topic.getModel(), viewProps); 351 } catch (Exception e) { 352 throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e); 353 } 354 } 355 356 // --- 357 358 private Association fetchTopicRefAssociation(long topicmapId, long topicId) { 359 return dm4.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC); 360 } 361 362 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) { 363 return dm4.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 364 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION); 365 } 366 367 // --- 368 369 private ViewProperties fetchViewProperties(Association mapcontextAssoc) { 370 int x = (Integer) mapcontextAssoc.getProperty(PROP_X); 371 int y = (Integer) mapcontextAssoc.getProperty(PROP_Y); 372 boolean visibility = (Boolean) mapcontextAssoc.getProperty(PROP_VISIBILITY); 373 return new ViewProperties(x, y, visibility); 374 } 375 376 // --- Store --- 377 378 private void storeViewProperties(long topicmapId, long topicId, ViewProperties viewProps) { 379 try { 380 Association assoc = fetchTopicRefAssociation(topicmapId, topicId); 381 if (assoc == null) { 382 throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId); 383 } 384 storeViewProperties(assoc, viewProps); 385 } catch (Exception e) { 386 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 387 "(viewProps=" + viewProps + ")", e); 388 } 389 } 390 391 private void storeViewProperties(Association mapcontextAssoc, ViewProperties viewProps) { 392 for (String propUri : viewProps) { 393 mapcontextAssoc.setProperty(propUri, viewProps.get(propUri), false); // addToIndex = false 394 } 395 } 396 397 // --- Viewmodel Customizers --- 398 399 private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) { 400 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 401 invokeViewmodelCustomizer(customizer, topic, viewProps); 402 } 403 } 404 405 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic, 406 ViewProperties viewProps) { 407 try { 408 customizer.enrichViewProperties(topic, viewProps); 409 } catch (Exception e) { 410 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 411 "(customizer=\"" + customizer.getClass().getName() + "\")", e); 412 } 413 } 414 415 // --- Topicmap Renderers --- 416 417 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 418 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 419 // 420 if (renderer == null) { 421 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 422 } 423 // 424 return renderer; 425 } 426 427 // --- 428 429 private InputStream invokeWebclient() { 430 return dm4.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html"); 431 } 432}