001 package de.deepamehta.plugins.topicmaps; 002 003 import de.deepamehta.plugins.topicmaps.model.TopicmapViewmodel; 004 import de.deepamehta.plugins.topicmaps.service.TopicmapsService; 005 006 import de.deepamehta.core.Association; 007 import de.deepamehta.core.Topic; 008 import de.deepamehta.core.model.AssociationModel; 009 import de.deepamehta.core.model.AssociationRoleModel; 010 import de.deepamehta.core.model.CompositeValueModel; 011 import de.deepamehta.core.model.TopicModel; 012 import de.deepamehta.core.model.TopicRoleModel; 013 import de.deepamehta.core.osgi.PluginActivator; 014 import de.deepamehta.core.service.ClientState; 015 import de.deepamehta.core.service.Directives; 016 import de.deepamehta.core.storage.spi.DeepaMehtaTransaction; 017 018 import javax.ws.rs.GET; 019 import javax.ws.rs.PUT; 020 import javax.ws.rs.POST; 021 import javax.ws.rs.DELETE; 022 import javax.ws.rs.HeaderParam; 023 import javax.ws.rs.Path; 024 import javax.ws.rs.PathParam; 025 import javax.ws.rs.Produces; 026 import javax.ws.rs.Consumes; 027 028 import java.io.InputStream; 029 import java.util.ArrayList; 030 import java.util.HashMap; 031 import java.util.List; 032 import java.util.Map; 033 import java.util.logging.Logger; 034 035 036 037 @Path("/topicmap") 038 @Consumes("application/json") 039 @Produces("application/json") 040 public class TopicmapsPlugin extends PluginActivator implements TopicmapsService { 041 042 // ------------------------------------------------------------------------------------------------------- Constants 043 044 private static final String DEFAULT_TOPICMAP_NAME = "untitled"; 045 private static final String DEFAULT_TOPICMAP_URI = "dm4.topicmaps.default_topicmap"; 046 private static final String DEFAULT_TOPICMAP_RENDERER = "dm4.webclient.default_topicmap_renderer"; 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 // ---------------------------------------------------------------------------------------------- Instance Variables 056 057 private Map<String, TopicmapRenderer> topicmapRenderers = new HashMap(); 058 private List<ViewmodelCustomizer> viewmodelCustomizers = new ArrayList(); 059 060 private Logger logger = Logger.getLogger(getClass().getName()); 061 062 // -------------------------------------------------------------------------------------------------- Public Methods 063 064 065 066 public TopicmapsPlugin() { 067 // Note: registering the default renderer in the InitializePluginListener would be too late. 068 // The renderer is already needed in the PostInstallPluginListener. 069 registerTopicmapRenderer(new DefaultTopicmapRenderer()); 070 } 071 072 073 074 // *************************************** 075 // *** TopicmapsService Implementation *** 076 // *************************************** 077 078 079 080 @GET 081 @Path("/{id}") 082 @Override 083 public TopicmapViewmodel getTopicmap(@PathParam("id") long topicmapId) { 084 try { 085 return new TopicmapViewmodel(topicmapId, dms, viewmodelCustomizers); 086 } catch (Exception e) { 087 throw new RuntimeException("Fetching topicmap " + topicmapId + " failed", e); 088 } 089 } 090 091 // --- 092 093 @POST 094 @Path("/{name}/{topicmap_renderer_uri}") 095 @Override 096 public Topic createTopicmap(@PathParam("name") String name, 097 @PathParam("topicmap_renderer_uri") String topicmapRendererUri, 098 @HeaderParam("Cookie") ClientState clientState) { 099 return createTopicmap(name, null, topicmapRendererUri, clientState); 100 } 101 102 @Override 103 public Topic createTopicmap(String name, String uri, String topicmapRendererUri, ClientState clientState) { 104 CompositeValueModel topicmapState = getTopicmapRenderer(topicmapRendererUri).initialTopicmapState(); 105 return dms.createTopic(new TopicModel(uri, "dm4.topicmaps.topicmap", new CompositeValueModel() 106 .put("dm4.topicmaps.name", name) 107 .put("dm4.topicmaps.topicmap_renderer_uri", topicmapRendererUri) 108 .put("dm4.topicmaps.state", topicmapState)), clientState); 109 } 110 111 // --- 112 113 @POST 114 @Path("/{id}/topic/{topic_id}") 115 @Override 116 public void addTopicToTopicmap(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 117 CompositeValueModel viewProps) { 118 DeepaMehtaTransaction tx = dms.beginTx(); 119 try { 120 dms.createAssociation(new AssociationModel(TOPIC_MAPCONTEXT, 121 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 122 new TopicRoleModel(topicId, ROLE_TYPE_TOPIC), viewProps), null); // FIXME: clientState=null 123 storeCustomViewProperties(topicmapId, topicId, viewProps); 124 // 125 tx.success(); 126 } catch (Exception e) { 127 throw new RuntimeException("Adding topic " + topicId + " to topicmap " + topicmapId + " failed " + 128 "(viewProps=" + viewProps + ")", e); 129 } finally { 130 tx.finish(); 131 } 132 } 133 134 @Override 135 public void addTopicToTopicmap(long topicmapId, long topicId, int x, int y, boolean visibility) { 136 addTopicToTopicmap(topicmapId, topicId, new StandardViewProperties(x, y, visibility)); 137 } 138 139 @POST 140 @Path("/{id}/association/{assoc_id}") 141 @Override 142 public void addAssociationToTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 143 dms.createAssociation(new AssociationModel(ASSOCIATION_MAPCONTEXT, 144 new TopicRoleModel(topicmapId, ROLE_TYPE_TOPICMAP), 145 new AssociationRoleModel(assocId, ROLE_TYPE_ASSOCIATION)), null); // FIXME: clientState=null 146 } 147 148 // --- 149 150 @Override 151 public boolean isTopicInTopicmap(long topicmapId, long topicId) { 152 return fetchTopicRefAssociation(topicmapId, topicId) != null; 153 } 154 155 // --- 156 157 @PUT 158 @Path("/{id}/topic/{topic_id}") 159 @Override 160 public void setViewProperties(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 161 CompositeValueModel viewProps) { 162 DeepaMehtaTransaction tx = dms.beginTx(); 163 try { 164 storeStandardViewProperties(topicmapId, topicId, viewProps); 165 storeCustomViewProperties(topicmapId, topicId, viewProps); 166 // 167 tx.success(); 168 } catch (Exception e) { 169 throw new RuntimeException("Storing view properties of topic " + topicId + " failed " + 170 "(viewProps=" + viewProps + ")", e); 171 } finally { 172 tx.finish(); 173 } 174 } 175 176 177 @PUT 178 @Path("/{id}/topic/{topic_id}/{x}/{y}") 179 @Override 180 public void setTopicPosition(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 181 @PathParam("x") int x, @PathParam("y") int y) { 182 storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(x, y)); 183 } 184 185 @PUT 186 @Path("/{id}/topic/{topic_id}/{visibility}") 187 @Override 188 public void setTopicVisibility(@PathParam("id") long topicmapId, @PathParam("topic_id") long topicId, 189 @PathParam("visibility") boolean visibility) { 190 storeStandardViewProperties(topicmapId, topicId, new StandardViewProperties(visibility)); 191 } 192 193 @DELETE 194 @Path("/{id}/association/{assoc_id}") 195 @Override 196 public void removeAssociationFromTopicmap(@PathParam("id") long topicmapId, @PathParam("assoc_id") long assocId) { 197 fetchAssociationRefAssociation(topicmapId, assocId).delete(new Directives()); 198 } 199 200 // --- 201 202 @PUT 203 @Path("/{id}") 204 @Override 205 public void setClusterPosition(@PathParam("id") long topicmapId, ClusterCoords coords) { 206 for (ClusterCoords.Entry entry : coords) { 207 setTopicPosition(topicmapId, entry.topicId, entry.x, entry.y); 208 } 209 } 210 211 @PUT 212 @Path("/{id}/translation/{x}/{y}") 213 @Override 214 public void setTopicmapTranslation(@PathParam("id") long topicmapId, @PathParam("x") int transX, 215 @PathParam("y") int transY) { 216 try { 217 CompositeValueModel topicmapState = new CompositeValueModel() 218 .put("dm4.topicmaps.state", new CompositeValueModel() 219 .put("dm4.topicmaps.translation", new CompositeValueModel() 220 .put("dm4.topicmaps.translation_x", transX) 221 .put("dm4.topicmaps.translation_y", transY))); 222 dms.updateTopic(new TopicModel(topicmapId, topicmapState), null); 223 } catch (Exception e) { 224 throw new RuntimeException("Setting translation of topicmap " + topicmapId + " failed (transX=" + 225 transX + ", transY=" + transY + ")", e); 226 } 227 } 228 229 // --- 230 231 @Override 232 public void registerTopicmapRenderer(TopicmapRenderer renderer) { 233 logger.info("### Registering topicmap renderer \"" + renderer.getClass().getName() + "\""); 234 topicmapRenderers.put(renderer.getUri(), renderer); 235 } 236 237 // --- 238 239 @Override 240 public void registerViewmodelCustomizer(ViewmodelCustomizer customizer) { 241 logger.info("### Registering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 242 viewmodelCustomizers.add(customizer); 243 } 244 245 @Override 246 public void unregisterViewmodelCustomizer(ViewmodelCustomizer customizer) { 247 logger.info("### Unregistering viewmodel customizer \"" + customizer.getClass().getName() + "\""); 248 if (!viewmodelCustomizers.remove(customizer)) { 249 throw new RuntimeException("Unregistering viewmodel customizer failed (customizer=" + customizer + ")"); 250 } 251 } 252 253 // --- 254 255 // Note: not part of topicmaps service 256 @GET 257 @Path("/{id}") 258 @Produces("text/html") 259 public InputStream getTopicmapInWebclient() { 260 // Note: the path parameter is evaluated at client-side 261 return invokeWebclient(); 262 } 263 264 // Note: not part of topicmaps service 265 @GET 266 @Path("/{id}/topic/{topic_id}") 267 @Produces("text/html") 268 public InputStream getTopicmapAndTopicInWebclient() { 269 // Note: the path parameters are evaluated at client-side 270 return invokeWebclient(); 271 } 272 273 274 275 // **************************** 276 // *** Hook Implementations *** 277 // **************************** 278 279 280 281 @Override 282 public void postInstall() { 283 createTopicmap(DEFAULT_TOPICMAP_NAME, DEFAULT_TOPICMAP_URI, DEFAULT_TOPICMAP_RENDERER, null); 284 // Note: null is passed as clientState. On post-install we have no clientState. 285 // The workspace assignment is made by the Access Control plugin on all-plugins-active. 286 } 287 288 289 290 // ------------------------------------------------------------------------------------------------- Private Methods 291 292 private void storeStandardViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) { 293 fetchTopicRefAssociation(topicmapId, topicId).setCompositeValue(viewProps, null, new Directives()); 294 } // clientState=null 295 296 // ### Note: the topicmapId parameter is not used. Per-topicmap custom view properties not yet supported. 297 private void storeCustomViewProperties(long topicmapId, long topicId, CompositeValueModel viewProps) { 298 invokeViewmodelCustomizers(topicId, viewProps); 299 } 300 301 // --- 302 303 private Association fetchTopicRefAssociation(long topicmapId, long topicId) { 304 return dms.getAssociation(TOPIC_MAPCONTEXT, topicmapId, topicId, 305 ROLE_TYPE_TOPICMAP, ROLE_TYPE_TOPIC, false); // fetchComposite=false 306 } 307 308 private Association fetchAssociationRefAssociation(long topicmapId, long assocId) { 309 return dms.getAssociationBetweenTopicAndAssociation(ASSOCIATION_MAPCONTEXT, topicmapId, assocId, 310 ROLE_TYPE_TOPICMAP, ROLE_TYPE_ASSOCIATION, false); // fetchComposite=false 311 } 312 313 // --- 314 315 // ### There is a copy in TopicmapViewmodel 316 private void invokeViewmodelCustomizers(long topicId, CompositeValueModel viewProps) { 317 Topic topic = dms.getTopic(topicId, false); // fetchComposite=false 318 for (ViewmodelCustomizer customizer : viewmodelCustomizers) { 319 invokeViewmodelCustomizer(customizer, topic, viewProps); 320 } 321 } 322 323 // ### There is a principal copy in TopicmapViewmodel 324 private void invokeViewmodelCustomizer(ViewmodelCustomizer customizer, Topic topic, CompositeValueModel viewProps) { 325 try { 326 customizer.storeViewProperties(topic, viewProps); 327 } catch (Exception e) { 328 throw new RuntimeException("Invoking viewmodel customizer for topic " + topic.getId() + " failed " + 329 "(customizer=\"" + customizer.getClass().getName() + "\", method=\"storeViewProperties\")", e); 330 } 331 } 332 333 // --- 334 335 private TopicmapRenderer getTopicmapRenderer(String rendererUri) { 336 TopicmapRenderer renderer = topicmapRenderers.get(rendererUri); 337 // 338 if (renderer == null) { 339 throw new RuntimeException("\"" + rendererUri + "\" is an unknown topicmap renderer"); 340 } 341 // 342 return renderer; 343 } 344 345 // --- 346 347 private InputStream invokeWebclient() { 348 try { 349 return dms.getPlugin("de.deepamehta.webclient").getResourceAsStream("web/index.html"); 350 } catch (Exception e) { 351 throw new RuntimeException("Invoking the webclient failed", e); 352 } 353 } 354 355 // --------------------------------------------------------------------------------------------- Private Inner Class 356 357 private class StandardViewProperties extends CompositeValueModel { 358 359 private StandardViewProperties(int x, int y, boolean visibility) { 360 put(x, y); 361 put(visibility); 362 } 363 364 private StandardViewProperties(int x, int y) { 365 put(x, y); 366 } 367 368 369 private StandardViewProperties(boolean visibility) { 370 put(visibility); 371 } 372 373 // --- 374 375 private void put(int x, int y) { 376 put("dm4.topicmaps.x", x); 377 put("dm4.topicmaps.y", y); 378 } 379 380 private void put(boolean visibility) { 381 put("dm4.topicmaps.visibility", visibility); 382 } 383 } 384 }