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.logging.Logger; 037 038 039 040@Path("/topicmap") 041@Consumes("application/json") 042@Produces("application/json") 043public 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 private static final String PROP_X = "dm4.topicmaps.x"; 055 private static final String PROP_Y = "dm4.topicmaps.y"; 056 private static final String PROP_VISIBILITY = "dm4.topicmaps.visibility"; 057 058 // ---------------------------------------------------------------------------------------------- Instance Variables 059 060 private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap(); 061 private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList(); 062 063 private Logger logger = Logger.getLogger(getClass().getName()); 064 065 // -------------------------------------------------------------------------------------------------- Public Methods 066 067 068 069 public TopicmapsPlugin() { 070 // Note: registering the default renderer in the init() hook would be too late. 071 // The renderer is already needed at install-in-DB time ### Still true? 072 registerTopicmapRenderer(new DefaultTopicmapRenderer()); 073 } 074 075 076 077 // *************************************** 078 // *** TopicmapsService Implementation *** 079 // *************************************** 080 081 082 083 @POST 084 @Transactional 085 @Override 086 public Topic createTopicmap(@QueryParam("name") String name, 087 @QueryParam("renderer_uri") String topicmapRendererUri, 088 @QueryParam("private") boolean isPrivate) { 089 logger.info("Creating topicmap \"" + name + "\" (topicmapRendererUri=\"" + topicmapRendererUri + 090 "\", isPrivate=" + isPrivate +")"); 091 return dm4.createTopic(mf.newTopicModel("dm4.topicmaps.topicmap", mf.newChildTopicsModel() 092 .put("dm4.topicmaps.name", name) 093 .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri) 094 .put("dm4.topicmaps.private", isPrivate) 095 .put("dm4.topicmaps.state", getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(mf)) 096 )); 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") long topicmapId, @PathParam("topic_id") long topicId, 137 ViewProperties viewProps) { 138 try { 139 if (isTopicInTopicmap(topicmapId, topicId)) { 140 throw new RuntimeException("The topic is already added"); 141 } 142 // 143 Association assoc = dm4.createAssociation(mf.newAssociationModel(TOPIC_MAPCONTEXT, 144 mf.newTopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 145 mf.newTopicRoleModel(topicId, ROLE_TYPE_TOPIC) 146 )); 147 storeViewProperties(assoc, viewProps); 148 } catch (Exception e) { 149 throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " + 150 "(viewProps=" + viewProps + ")", e); 151 } 152 } 153 154 @Override 155 public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) { 156 addTopicToTopicmap(topicmapId, topicId, new ViewProperties(x, y, visibility)); 157 } 158 159 @POST 160 @Path("/{id}/association/{assoc_id}") 161 @Transactional 162 @Override 163 public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 164 try { 165 if (isAssociationInTopicmap(topicmapId, assocId)) { 166 throw new RuntimeException("The association is already added"); 167 } 168 // 169 dm4.createAssociation(mf.newAssociationModel(ASSOCIATION_MAPCONTEXT, 170 mf.newTopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 171 mf.newAssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION) 172 )); 173 } catch (Exception e) { 174 throw new RuntimeException("Adding association " + assocId + " to topicmap " + topicmapId + " failed", e); 175 } 176 } 177 178 // --- 179 180 @PUT 181 @Path("/{id}/topic/{topic_id}") 182 @Transactional 183 @Override 184 public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 185 ViewProperties viewProps) { 186 storeViewProperties(topicmapId, topicId, viewProps); 187 } 188 189 190 @PUT 191 @Path("/{id}/topic/{topic_id}/{x}/{y}") 192 @Transactional 193 @Override 194 public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 195 @PathParam("x") int x, @PathParam("y") int y) { 196 storeViewProperties(topicmapId, topicId, new ViewProperties(x, y)); 197 } 198 199 @PUT 200 @Path("/{id}/topic/{topic_id}/{visibility}") 201 @Transactional 202 @Override 203 public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 204 @PathParam("visibility") boolean visibility) { 205 storeViewProperties(topicmapId, topicId, new ViewProperties(visibility)); 206 } 207 208 @DELETE 209 @Path("/{id}/association/{assoc_id}") 210 @Transactional 211 @Override 212 public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 213 try { 214 Association assoc = fetchAssociationRefAssociation(topicmapId, assocId); 215 if (assoc == null) { 216 throw new RuntimeException("Association " + assocId + " is not contained in topicmap " + topicmapId); 217 } 218 assoc.delete(); 219 } catch (Exception e) { 220 throw new RuntimeException("Removing association " + assocId + " from topicmap " + topicmapId + " failed ", 221 e); 222 } 223 } 224 225 // --- 226 227 @PUT 228 @Path("/{id}") 229 @Transactional 230 @Override 231 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 232 for (ClusterCoords.Entry entry : coords) { 233 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 234 } 235 } 236 237 @PUT 238 @Path("/{id}/translation/{x}/{y}") 239 @Transactional 240 @Override 241 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 242 @PathParam("y") int transY) { 243 try { 244 ChildTopicsModel topicmapState = mf.newChildTopicsModel() 245 .put("dm4.topicmaps.state", mf.newChildTopicsModel() 246 .put("dm4.topicmaps.translation", mf.newChildTopicsModel() 247 .put("dm4.topicmaps.translation_x", transX) 248 .put("dm4.topicmaps.translation_y", transY))); 249 dm4.updateTopic(mf.newTopicModel(topicmapId, topicmapState)); 250 } catch (Exception e) { 251 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 252 transX + ", transY=" + transY + ")", e); 253 } 254 } 255 256 // --- 257 258 @Override 259 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 260 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 261 topicmapRenderers.put(renderer.getUri(), renderer); 262 } 263 264 // --- 265 266 @Override 267 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 268 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 269 viewmodelCustomizers.add(customizer); 270 } 271 272 @Override 273 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 274 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 275 if (!viewmodelCustomizers.remove(customizer)) { 276 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 277 } 278 } 279 280 // --- 281 282 // Note: not part of topicmaps service 283 @GET 284 @Path("/{id}") 285 @Produces("text/html") 286 public InputStream getTopicmapInWebclient() { 287 // Note: the path parameter is evaluated at client-side 288 return invokeWebclient(); 289 } 290 291 // Note: not part of topicmaps service 292 @GET 293 @Path("/{id}/topic/{topic_id}") 294 @Produces("text/html") 295 public InputStream getTopicmapAndTopicInWebclient() { 296 // Note: the path parameters are evaluated at client-side 297 return invokeWebclient(); 298 } 299 300 301 302 // ------------------------------------------------------------------------------------------------- Private Methods 303 304 // --- Fetch --- 305 306 private Map<Long, TopicViewModel> fetchTopics(Topic topicmapTopic, boolean includeChilds) { 307 Map<Long, TopicViewModel> topics = new HashMap(); 308 List<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, "dm4.core.default", 309 "dm4.topicmaps.topicmap_topic", null); // othersTopicTypeUri=null 310 if (includeChilds) { 311 DeepaMehtaUtils.loadChildTopics(relTopics); 312 } 313 for (RelatedTopic topic : relTopics) { 314 topics.put(topic.getId(), createTopicViewModel(topic)); 315 } 316 return topics; 317 } 318 319 private Map<Long, AssociationViewModel> fetchAssociations(Topic topicmapTopic) { 320 Map<Long, AssociationViewModel> assocs = new HashMap(); 321 List<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT, 322 "dm4.core.default", "dm4.topicmaps.topicmap_association", null); 323 for (RelatedAssociation assoc : relAssocs) { 324 assocs.put(assoc.getId(), mf.newAssociationViewModel(assoc.getModel())); 325 } 326 return assocs; 327 } 328 329 // --- 330 331 private TopicViewModel createTopicViewModel(RelatedTopic topic) { 332 try { 333 ViewProperties viewProps = fetchViewProperties(topic.getRelatingAssociation()); 334 invokeViewmodelCustomizers(topic, viewProps); 335 return mf.newTopicViewModel(topic.getModel(), viewProps); 336 } catch (Exception e) { 337 throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e); 338 } 339 } 340 341 // --- 342 343 private Association fetchTopicRefAssociation(long topicmapId, long topicId) { 344 return dm4.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC); 345 } 346 347 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) { 348 return dm4.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 349 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION); 350 } 351 352 // --- 353 354 private ViewProperties fetchViewProperties(Association mapcontextAssoc) { 355 int x = (Integer) mapcontextAssoc.getProperty(PROP_X); 356 int y = (Integer) mapcontextAssoc.getProperty(PROP_Y); 357 boolean visibility = (Boolean) mapcontextAssoc.getProperty(PROP_VISIBILITY); 358 return new ViewProperties(x, y, visibility); 359 } 360 361 // --- Store --- 362 363 private void storeViewProperties(long topicmapId, long topicId, ViewProperties viewProps) { 364 try { 365 Association assoc = fetchTopicRefAssociation(topicmapId, topicId); 366 if (assoc == null) { 367 throw new RuntimeException("Topic " + topicId + " is not contained in topicmap " + topicmapId); 368 } 369 storeViewProperties(assoc, viewProps); 370 } catch (Exception e) { 371 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 372 "(viewProps=" + viewProps + ")", e); 373 } 374 } 375 376 private void storeViewProperties(Association mapcontextAssoc, ViewProperties viewProps) { 377 for (String propUri : viewProps) { 378 mapcontextAssoc.setProperty(propUri, viewProps.get(propUri), false); // addToIndex = false 379 } 380 } 381 382 // --- Viewmodel Customizers --- 383 384 private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) { 385 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 386 invokeViewmodelCustomizer(customizer, topic, viewProps); 387 } 388 } 389 390 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic, 391 ViewProperties viewProps) { 392 try { 393 customizer.enrichViewProperties(topic, viewProps); 394 } catch (Exception e) { 395 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 396 "(customizer=\"" + customizer.getClass().getName() + "\")", e); 397 } 398 } 399 400 // --- Topicmap Renderers --- 401 402 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 403 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 404 // 405 if (renderer == null) { 406 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 407 } 408 // 409 return renderer; 410 } 411 412 // --- 413 414 private InputStream invokeWebclient() { 415 return dm4.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html"); 416 } 417}