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 // --- 125 126 @POST 127 @Path("/{id}/topic/{topic_id}") 128 @Transactional 129 @Override 130 public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 131 ViewProperties viewProps) { 132 try { 133 Association assoc = dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT, 134 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 135 new TopicRoleModel(topicId, ROLE_TYPE_TOPIC) 136 )); 137 storeViewProperties(assoc, viewProps); 138 } catch (Exception e) { 139 throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " + 140 "(viewProps=" + viewProps + ")", e); 141 } 142 } 143 144 @Override 145 public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) { 146 addTopicToTopicmap(topicmapId, topicId, new ViewProperties(x, y, visibility)); 147 } 148 149 @POST 150 @Path("/{id}/association/{assoc_id}") 151 @Transactional 152 @Override 153 public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 154 dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT, 155 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 156 new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION) 157 )); 158 } 159 160 // --- 161 162 @PUT 163 @Path("/{id}/topic/{topic_id}") 164 @Transactional 165 @Override 166 public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 167 ViewProperties viewProps) { 168 try { 169 storeViewProperties(topicmapId, topicId, viewProps); 170 } catch (Exception e) { 171 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 172 "(viewProps=" + viewProps + ")", e); 173 } 174 } 175 176 177 @PUT 178 @Path("/{id}/topic/{topic_id}/{x}/{y}") 179 @Transactional 180 @Override 181 public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 182 @PathParam("x") int x, @PathParam("y") int y) { 183 storeViewProperties(topicmapId, topicId, new ViewProperties(x, y)); 184 } 185 186 @PUT 187 @Path("/{id}/topic/{topic_id}/{visibility}") 188 @Transactional 189 @Override 190 public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 191 @PathParam("visibility") boolean visibility) { 192 storeViewProperties(topicmapId, topicId, new ViewProperties(visibility)); 193 } 194 195 @DELETE 196 @Path("/{id}/association/{assoc_id}") 197 @Transactional 198 @Override 199 public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 200 fetchAssociationRefAssociation(topicmapId, assocId).delete(); 201 } 202 203 // --- 204 205 @PUT 206 @Path("/{id}") 207 @Transactional 208 @Override 209 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 210 for (ClusterCoords.Entry entry : coords) { 211 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 212 } 213 } 214 215 @PUT 216 @Path("/{id}/translation/{x}/{y}") 217 @Transactional 218 @Override 219 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 220 @PathParam("y") int transY) { 221 try { 222 ChildTopicsModel topicmapState = new ChildTopicsModel() 223 .put("dm4.topicmaps.state", new ChildTopicsModel() 224 .put("dm4.topicmaps.translation", new ChildTopicsModel() 225 .put("dm4.topicmaps.translation_x", transX) 226 .put("dm4.topicmaps.translation_y", transY))); 227 dms.updateTopic(new TopicModel(topicmapId, topicmapState)); 228 } catch (Exception e) { 229 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 230 transX + ", transY=" + transY + ")", e); 231 } 232 } 233 234 // --- 235 236 @Override 237 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 238 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 239 topicmapRenderers.put(renderer.getUri(), renderer); 240 } 241 242 // --- 243 244 @Override 245 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 246 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 247 viewmodelCustomizers.add(customizer); 248 } 249 250 @Override 251 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 252 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 253 if (!viewmodelCustomizers.remove(customizer)) { 254 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 255 } 256 } 257 258 // --- 259 260 // Note: not part of topicmaps service 261 @GET 262 @Path("/{id}") 263 @Produces("text/html") 264 public InputStream getTopicmapInWebclient() { 265 // Note: the path parameter is evaluated at client-side 266 return invokeWebclient(); 267 } 268 269 // Note: not part of topicmaps service 270 @GET 271 @Path("/{id}/topic/{topic_id}") 272 @Produces("text/html") 273 public InputStream getTopicmapAndTopicInWebclient() { 274 // Note: the path parameters are evaluated at client-side 275 return invokeWebclient(); 276 } 277 278 279 280 // ------------------------------------------------------------------------------------------------- Private Methods 281 282 // --- Fetch --- 283 284 private Map<Long, TopicViewmodel> fetchTopics(Topic topicmapTopic, boolean includeChilds) { 285 Map<Long, TopicViewmodel> topics = new HashMap(); 286 ResultList<RelatedTopic> relTopics = topicmapTopic.getRelatedTopics(TOPIC_MAPCONTEXT, 287 "dm4.core.default", "dm4.topicmaps.topicmap_topic", null, 0); // othersTopicTypeUri=null, maxResultSize=0 288 if (includeChilds) { 289 relTopics.loadChildTopics(); 290 } 291 for (RelatedTopic topic : relTopics) { 292 topics.put(topic.getId(), createTopicViewmodel(topic)); 293 } 294 return topics; 295 } 296 297 private Map<Long, AssociationViewmodel> fetchAssociations(Topic topicmapTopic) { 298 Map<Long, AssociationViewmodel> assocs = new HashMap(); 299 ResultList<RelatedAssociation> relAssocs = topicmapTopic.getRelatedAssociations(ASSOCIATION_MAPCONTEXT, 300 "dm4.core.default", "dm4.topicmaps.topicmap_association", null); 301 for (RelatedAssociation assoc : relAssocs) { 302 assocs.put(assoc.getId(), new AssociationViewmodel(assoc.getModel())); 303 } 304 return assocs; 305 } 306 307 // --- 308 309 private TopicViewmodel createTopicViewmodel(RelatedTopic topic) { 310 try { 311 ViewProperties viewProps = fetchViewProperties(topic.getRelatingAssociation()); 312 invokeViewmodelCustomizers(topic, viewProps); 313 return new TopicViewmodel(topic.getModel(), viewProps); 314 } catch (Exception e) { 315 throw new RuntimeException("Creating viewmodel for topic " + topic.getId() + " failed", e); 316 } 317 } 318 319 // --- 320 321 private Association fetchTopicRefAssociation(long topicmapId, long topicId) { 322 return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC); 323 } 324 325 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) { 326 return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 327 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION); 328 } 329 330 // --- 331 332 private ViewProperties fetchViewProperties(Association mapcontextAssoc) { 333 int x = (Integer) mapcontextAssoc.getProperty(PROP_X); 334 int y = (Integer) mapcontextAssoc.getProperty(PROP_Y); 335 boolean visibility = (Boolean) mapcontextAssoc.getProperty(PROP_VISIBILITY); 336 return new ViewProperties(x, y, visibility); 337 } 338 339 // --- Store --- 340 341 private void storeViewProperties(long topicmapId, long topicId, ViewProperties viewProps) { 342 storeViewProperties(fetchTopicRefAssociation(topicmapId, topicId), viewProps); 343 } 344 345 private void storeViewProperties(Association mapcontextAssoc, ViewProperties viewProps) { 346 for (String propUri : viewProps.propUris()) { 347 mapcontextAssoc.setProperty(propUri, viewProps.get(propUri), false); // addToIndex = false 348 } 349 } 350 351 // --- Viewmodel Customizers --- 352 353 private void invokeViewmodelCustomizers(RelatedTopic topic, ViewProperties viewProps) { 354 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 355 invokeViewmodelCustomizer(customizer, topic, viewProps); 356 } 357 } 358 359 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, RelatedTopic topic, 360 ViewProperties viewProps) { 361 try { 362 customizer.enrichViewProperties(topic, viewProps); 363 } catch (Exception e) { 364 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 365 "(customizer=\"" + customizer.getClass().getName() + "\")", e); 366 } 367 } 368 369 // --- Topicmap Renderers --- 370 371 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 372 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 373 // 374 if (renderer == null) { 375 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 376 } 377 // 378 return renderer; 379 } 380 381 // --- 382 383 private InputStream invokeWebclient() { 384 return dms.getPlugin("de.deepamehta.webclient").getStaticResource("/web/index.html"); 385 } 386 }